From 0e9c286a57c70f0744348b10644fab5ad5f7e34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E7=89=A9?= <253518756@qq.com> Date: Wed, 22 Apr 2026 20:14:15 +0800 Subject: [PATCH] 1 --- .env.example | 7 + AGENTS.md | 3 +- ...ME_CREATION_AND_GAMEPLAY_PRD_2026-04-22.md | 1180 +++++++++++ ...ATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md | 1109 ++++++++++ ...NTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md | 273 +++ ...NTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md | 384 ++++ docs/technical/README.md | 3 + ...REATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md | 126 ++ packages/shared/src/contracts/bigFish.ts | 190 ++ .../src/contracts/puzzleAgentActions.ts | 59 + .../shared/src/contracts/puzzleAgentDraft.ts | 58 + .../src/contracts/puzzleAgentSession.ts | 58 + .../src/contracts/puzzleResultPreview.ts | 21 + .../src/contracts/puzzleRuntimeSession.ts | 74 + .../shared/src/contracts/puzzleWorkSummary.ts | 39 + .../src/contracts/rpgCreationFixtures.ts | 2 +- packages/shared/src/index.ts | 7 + scripts/dev-node.mjs | 97 +- server-node/src/app.ts | 14 +- server-node/src/routes/bigFishProxyRoutes.ts | 329 +++ server-node/src/routes/puzzleProxyRoutes.ts | 439 ++++ server-rs/Cargo.lock | 26 + server-rs/Cargo.toml | 2 + server-rs/crates/api-server/Cargo.toml | 1 + server-rs/crates/api-server/src/app.rs | 159 ++ server-rs/crates/api-server/src/auth.rs | 89 +- server-rs/crates/api-server/src/big_fish.rs | 653 ++++++ server-rs/crates/api-server/src/config.rs | 6 + server-rs/crates/api-server/src/main.rs | 2 + server-rs/crates/api-server/src/puzzle.rs | 1394 ++++++++++++ .../crates/api-server/src/runtime_story.rs | 142 +- server-rs/crates/module-big-fish/Cargo.toml | 15 + server-rs/crates/module-big-fish/src/lib.rs | 1385 ++++++++++++ .../crates/module-custom-world/src/lib.rs | 170 ++ server-rs/crates/module-puzzle/Cargo.toml | 15 + server-rs/crates/module-puzzle/src/lib.rs | 1538 ++++++++++++++ .../crates/shared-contracts/src/big_fish.rs | 238 +++ server-rs/crates/shared-contracts/src/lib.rs | 5 + .../shared-contracts/src/puzzle_agent.rs | 188 ++ .../shared-contracts/src/puzzle_gallery.rs | 15 + .../shared-contracts/src/puzzle_runtime.rs | 99 + .../shared-contracts/src/puzzle_works.rs | 65 + server-rs/crates/spacetime-client/Cargo.toml | 2 + server-rs/crates/spacetime-client/src/lib.rs | 1865 +++++++++++++++++ .../advance_puzzle_next_level_procedure.rs | 58 + .../big_fish_agent_message_kind_type.rs | 31 + .../big_fish_agent_message_role_type.rs | 29 + .../big_fish_agent_message_snapshot_type.rs | 30 + .../big_fish_agent_message_table.rs | 165 ++ .../big_fish_agent_message_type.rs | 79 + .../big_fish_anchor_item_type.rs | 27 + .../big_fish_anchor_pack_type.rs | 27 + .../big_fish_anchor_status_type.rs | 31 + .../big_fish_asset_coverage_type.rs | 28 + .../big_fish_asset_generate_input_type.rs | 29 + .../big_fish_asset_kind_type.rs | 29 + .../big_fish_asset_slot_snapshot_type.rs | 33 + .../big_fish_asset_slot_table.rs | 165 ++ .../big_fish_asset_slot_type.rs | 88 + .../big_fish_asset_status_type.rs | 27 + .../big_fish_background_blueprint_type.rs | 30 + .../big_fish_creation_session_table.rs | 164 ++ .../big_fish_creation_session_type.rs | 99 + .../big_fish_creation_stage_type.rs | 33 + .../big_fish_draft_compile_input_type.rs | 25 + .../big_fish_game_draft_type.rs | 32 + .../big_fish_level_blueprint_type.rs | 33 + .../big_fish_message_submit_input_type.rs | 28 + .../big_fish_publish_input_type.rs | 25 + .../big_fish_run_get_input_type.rs | 24 + .../big_fish_run_input_submit_input_type.rs | 27 + .../big_fish_run_procedure_result_type.rs | 26 + .../big_fish_run_start_input_type.rs | 26 + .../big_fish_run_status_type.rs | 29 + .../big_fish_runtime_entity_type.rs | 28 + .../big_fish_runtime_params_type.rs | 31 + .../big_fish_runtime_run_table.rs | 164 ++ .../big_fish_runtime_run_type.rs | 92 + .../big_fish_runtime_snapshot_type.rs | 38 + .../big_fish_session_create_input_type.rs | 28 + .../big_fish_session_get_input_type.rs | 24 + .../big_fish_session_procedure_result_type.rs | 26 + .../big_fish_session_snapshot_type.rs | 43 + .../module_bindings/big_fish_vector_2_type.rs | 24 + .../compile_big_fish_draft_procedure.rs | 58 + .../compile_puzzle_agent_draft_procedure.rs | 58 + .../create_big_fish_session_procedure.rs | 58 + .../create_puzzle_agent_session_procedure.rs | 58 + .../drag_puzzle_piece_or_group_procedure.rs | 58 + .../generate_big_fish_asset_procedure.rs | 58 + .../get_big_fish_run_procedure.rs | 58 + .../get_big_fish_session_procedure.rs | 58 + .../get_puzzle_agent_session_procedure.rs | 58 + .../get_puzzle_gallery_detail_procedure.rs | 58 + .../get_puzzle_run_procedure.rs | 58 + .../get_puzzle_work_detail_procedure.rs | 58 + .../list_puzzle_gallery_procedure.rs | 53 + .../list_puzzle_works_procedure.rs | 58 + .../src/module_bindings/mod.rs | 264 +++ .../publish_big_fish_game_procedure.rs | 58 + .../publish_puzzle_work_procedure.rs | 58 + .../puzzle_agent_message_kind_type.rs | 31 + .../puzzle_agent_message_role_type.rs | 29 + .../puzzle_agent_message_row_type.rs | 79 + .../puzzle_agent_message_submit_input_type.rs | 27 + .../puzzle_agent_message_table.rs | 165 ++ .../puzzle_agent_session_create_input_type.rs | 28 + .../puzzle_agent_session_get_input_type.rs | 24 + ...zle_agent_session_procedure_result_type.rs | 25 + .../puzzle_agent_session_row_type.rs | 96 + .../puzzle_agent_session_table.rs | 164 ++ .../puzzle_agent_stage_type.rs | 33 + .../puzzle_draft_compile_input_type.rs | 25 + ...puzzle_generated_images_save_input_type.rs | 26 + .../puzzle_publication_status_type.rs | 27 + .../puzzle_publish_input_type.rs | 31 + .../puzzle_run_drag_input_type.rs | 28 + .../puzzle_run_get_input_type.rs | 24 + .../puzzle_run_next_level_input_type.rs | 25 + .../puzzle_run_procedure_result_type.rs | 25 + .../puzzle_run_start_input_type.rs | 26 + .../puzzle_run_swap_input_type.rs | 27 + .../puzzle_runtime_run_row_type.rs | 95 + .../puzzle_runtime_run_table.rs | 163 ++ .../puzzle_select_cover_image_input_type.rs | 26 + .../puzzle_work_get_input_type.rs | 23 + .../puzzle_work_procedure_result_type.rs | 25 + .../puzzle_work_profile_row_type.rs | 113 + .../puzzle_work_profile_table.rs | 164 ++ .../puzzle_work_upsert_input_type.rs | 30 + .../puzzle_works_list_input_type.rs | 23 + .../puzzle_works_procedure_result_type.rs | 25 + .../save_puzzle_generated_images_procedure.rs | 58 + .../select_puzzle_cover_image_procedure.rs | 58 + .../start_big_fish_run_procedure.rs | 58 + .../start_puzzle_run_procedure.rs | 58 + .../submit_big_fish_input_procedure.rs | 58 + .../submit_big_fish_message_procedure.rs | 58 + .../submit_puzzle_agent_message_procedure.rs | 58 + .../swap_puzzle_pieces_procedure.rs | 58 + .../update_puzzle_work_procedure.rs | 58 + server-rs/crates/spacetime-module/Cargo.toml | 3 + server-rs/crates/spacetime-module/src/lib.rs | 870 ++++++++ .../crates/spacetime-module/src/puzzle.rs | 1419 +++++++++++++ server-rs/scripts/dev.ps1 | 2 +- server-rs/scripts/dev.sh | 2 +- src/components/auth/AccountModal.test.tsx | 10 + .../BigFishAgentWorkspace.tsx | 102 + .../big-fish-result/BigFishResultView.tsx | 471 +++++ .../big-fish-runtime/BigFishRuntimeShell.tsx | 230 ++ .../CreationAgentWorkspace.test.tsx | 112 + .../creation-agent/CreationAgentWorkspace.tsx | 493 +++++ src/components/creation-agent/index.ts | 1 + .../CustomWorldAgentComposer.tsx | 74 - .../CustomWorldAgentHeader.tsx | 17 - .../CustomWorldAgentOperationBanner.tsx | 78 - .../CustomWorldAgentThread.test.tsx | 93 - .../CustomWorldAgentThread.tsx | 111 - .../CustomWorldAgentWorkspace.tsx | 239 ++- .../EightAnchorProgressBar.tsx | 111 - ...ustomWorldCreationHub.interaction.test.tsx | 39 + .../CustomWorldCreationHub.test.tsx | 38 + .../CustomWorldCreationHub.tsx | 59 +- .../custom-world-home/CustomWorldWorkCard.tsx | 131 +- .../PlatformEntryCreationTypeModal.tsx | 180 ++ .../platform-entry/PlatformEntryFlowShell.tsx | 17 + .../PlatformEntryFlowShellImpl.tsx | 1504 +++++++++++++ .../platform-entry/PlatformEntryHomeView.tsx | 9 + .../PlatformEntryWorldDetailView.tsx | 8 + src/components/platform-entry/index.ts | 9 + .../platform-entry/platformEntryShared.ts | 9 + .../platform-entry/platformEntryTypes.ts | 41 + .../usePlatformEntryBootstrap.ts | 5 + .../usePlatformEntryLibraryDetail.ts | 5 + .../usePlatformEntryNavigation.ts | 5 + .../puzzle-agent/PuzzleAgentWorkspace.tsx | 118 ++ .../PuzzleGalleryDetailView.tsx | 115 + .../puzzle-result/PuzzleResultView.tsx | 475 +++++ .../puzzle-runtime/PuzzleRuntimeShell.tsx | 397 ++++ .../rpg-entry/RpgEntryCreationTypeModal.tsx | 160 +- .../rpg-entry/RpgEntryFlowShell.tsx | 8 +- .../rpg-entry/RpgEntryFlowShellImpl.tsx | 723 +------ src/components/rpg-entry/rpgEntryTypes.ts | 48 +- .../RpgRuntimeStageRouter.tsx | 10 +- .../useRpgRuntimeOverlayState.ts | 2 +- src/data/customWorldLibrary.ts | 11 +- src/hooks/useResolvedAssetReadUrl.ts | 7 +- .../bigFishCreationClient.ts | 147 ++ src/services/big-fish-creation/index.ts | 8 + .../big-fish-runtime/bigFishRuntimeClient.ts | 68 + src/services/big-fish-runtime/index.ts | 6 + .../creation-agent/creationAgentProgress.ts | 77 + .../creation-agent/creationAgentSse.ts | 105 + src/services/creation-agent/index.ts | 2 + src/services/customWorldCover.test.ts | 9 +- src/services/platform-entry/index.ts | 5 + src/services/puzzle-agent/index.ts | 8 + .../puzzle-agent/puzzleAgentClient.ts | 168 ++ src/services/puzzle-gallery/index.ts | 5 + .../puzzle-gallery/puzzleGalleryClient.ts | 49 + src/services/puzzle-runtime/index.ts | 8 + .../puzzle-runtime/puzzleRuntimeClient.ts | 120 ++ src/services/puzzle-works/index.ts | 6 + .../puzzle-works/puzzleWorksClient.ts | 85 + .../rpg-creation/rpgCreationAgentClient.ts | 84 +- 205 files changed, 25790 insertions(+), 1623 deletions(-) create mode 100644 docs/prd/AI_NATIVE_AGENT_FIRST_BIG_FISH_GAME_CREATION_AND_GAMEPLAY_PRD_2026-04-22.md create mode 100644 docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md create mode 100644 docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md create mode 100644 docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md create mode 100644 docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md create mode 100644 packages/shared/src/contracts/bigFish.ts create mode 100644 packages/shared/src/contracts/puzzleAgentActions.ts create mode 100644 packages/shared/src/contracts/puzzleAgentDraft.ts create mode 100644 packages/shared/src/contracts/puzzleAgentSession.ts create mode 100644 packages/shared/src/contracts/puzzleResultPreview.ts create mode 100644 packages/shared/src/contracts/puzzleRuntimeSession.ts create mode 100644 packages/shared/src/contracts/puzzleWorkSummary.ts create mode 100644 server-node/src/routes/bigFishProxyRoutes.ts create mode 100644 server-node/src/routes/puzzleProxyRoutes.ts create mode 100644 server-rs/crates/api-server/src/big_fish.rs create mode 100644 server-rs/crates/api-server/src/puzzle.rs create mode 100644 server-rs/crates/module-big-fish/Cargo.toml create mode 100644 server-rs/crates/module-big-fish/src/lib.rs create mode 100644 server-rs/crates/module-puzzle/Cargo.toml create mode 100644 server-rs/crates/module-puzzle/src/lib.rs create mode 100644 server-rs/crates/shared-contracts/src/big_fish.rs create mode 100644 server-rs/crates/shared-contracts/src/puzzle_agent.rs create mode 100644 server-rs/crates/shared-contracts/src/puzzle_gallery.rs create mode 100644 server-rs/crates/shared-contracts/src/puzzle_runtime.rs create mode 100644 server-rs/crates/shared-contracts/src/puzzle_works.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_kind_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_role_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_item_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_pack_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_status_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_coverage_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_kind_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_status_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_background_blueprint_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_stage_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_draft_compile_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_game_draft_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_level_blueprint_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_message_submit_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_publish_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_input_submit_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_start_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_status_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_params_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_create_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_snapshot_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_kind_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_role_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_submit_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_create_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_stage_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_compile_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_images_save_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_publication_status_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_publish_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_drag_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_start_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_swap_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_select_cover_image_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_upsert_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_list_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/start_big_fish_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_input_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs create mode 100644 server-rs/crates/spacetime-module/src/puzzle.rs create mode 100644 src/components/big-fish-creation/BigFishAgentWorkspace.tsx create mode 100644 src/components/big-fish-result/BigFishResultView.tsx create mode 100644 src/components/big-fish-runtime/BigFishRuntimeShell.tsx create mode 100644 src/components/creation-agent/CreationAgentWorkspace.test.tsx create mode 100644 src/components/creation-agent/CreationAgentWorkspace.tsx create mode 100644 src/components/creation-agent/index.ts delete mode 100644 src/components/custom-world-agent/CustomWorldAgentComposer.tsx delete mode 100644 src/components/custom-world-agent/CustomWorldAgentHeader.tsx delete mode 100644 src/components/custom-world-agent/CustomWorldAgentOperationBanner.tsx delete mode 100644 src/components/custom-world-agent/CustomWorldAgentThread.test.tsx delete mode 100644 src/components/custom-world-agent/CustomWorldAgentThread.tsx delete mode 100644 src/components/custom-world-agent/EightAnchorProgressBar.tsx create mode 100644 src/components/platform-entry/PlatformEntryCreationTypeModal.tsx create mode 100644 src/components/platform-entry/PlatformEntryFlowShell.tsx create mode 100644 src/components/platform-entry/PlatformEntryFlowShellImpl.tsx create mode 100644 src/components/platform-entry/PlatformEntryHomeView.tsx create mode 100644 src/components/platform-entry/PlatformEntryWorldDetailView.tsx create mode 100644 src/components/platform-entry/index.ts create mode 100644 src/components/platform-entry/platformEntryShared.ts create mode 100644 src/components/platform-entry/platformEntryTypes.ts create mode 100644 src/components/platform-entry/usePlatformEntryBootstrap.ts create mode 100644 src/components/platform-entry/usePlatformEntryLibraryDetail.ts create mode 100644 src/components/platform-entry/usePlatformEntryNavigation.ts create mode 100644 src/components/puzzle-agent/PuzzleAgentWorkspace.tsx create mode 100644 src/components/puzzle-gallery/PuzzleGalleryDetailView.tsx create mode 100644 src/components/puzzle-result/PuzzleResultView.tsx create mode 100644 src/components/puzzle-runtime/PuzzleRuntimeShell.tsx create mode 100644 src/services/big-fish-creation/bigFishCreationClient.ts create mode 100644 src/services/big-fish-creation/index.ts create mode 100644 src/services/big-fish-runtime/bigFishRuntimeClient.ts create mode 100644 src/services/big-fish-runtime/index.ts create mode 100644 src/services/creation-agent/creationAgentProgress.ts create mode 100644 src/services/creation-agent/creationAgentSse.ts create mode 100644 src/services/creation-agent/index.ts create mode 100644 src/services/platform-entry/index.ts create mode 100644 src/services/puzzle-agent/index.ts create mode 100644 src/services/puzzle-agent/puzzleAgentClient.ts create mode 100644 src/services/puzzle-gallery/index.ts create mode 100644 src/services/puzzle-gallery/puzzleGalleryClient.ts create mode 100644 src/services/puzzle-runtime/index.ts create mode 100644 src/services/puzzle-runtime/puzzleRuntimeClient.ts create mode 100644 src/services/puzzle-works/index.ts create mode 100644 src/services/puzzle-works/puzzleWorksClient.ts diff --git a/.env.example b/.env.example index ec4cadc0..44493fe4 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,13 @@ VITE_SCENE_IMAGE_PROXY_BASE_URL="/api/custom-world/scene-image" NODE_SERVER_ADDR=":8081" NODE_SERVER_TARGET="http://127.0.0.1:8081" +# Rust api-server local target used by the Big Fish compatibility gateway in server-node. +GENARRATIVE_API_PORT="3100" +GENARRATIVE_API_TARGET="http://127.0.0.1:3100" +GENARRATIVE_INTERNAL_API_SECRET="CHANGE_ME_FOR_PRODUCTION" +GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3001" +GENARRATIVE_SPACETIME_DATABASE="genarrative-dev" + # Local Caddy upstream target used for dist-based testing. CADDY_API_UPSTREAM="http://127.0.0.1:8081" diff --git a/AGENTS.md b/AGENTS.md index 9ddf2f74..6815ea28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,8 @@ - UI设计需要兼顾网页端、移动端双端的使用体验,确保在不同设备上都能正常显示和操作,移动端优先考虑。 - 不要在gitignore中添加.env.local文件。 - 严格遵循简洁的代码风格 -- 前端只负责做表现,所有的逻辑、数据都放到Express后端进行运算和存储。 +- 前端只负责做表现,所有的逻辑、数据都放到后端工程,后端使用server-rs中用Rust+spacetimeDB的方案实现,禁止继续使用server-node(Express)和postgreSQL +- 后端采用多crate设计 - 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。 - 禁止将功能说明描述类的文本默认写入UI界面中。 - prd文档中每个模块的描述要落地设计到可以精准编码到位,不能出现需求落地漂移。 diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_BIG_FISH_GAME_CREATION_AND_GAMEPLAY_PRD_2026-04-22.md b/docs/prd/AI_NATIVE_AGENT_FIRST_BIG_FISH_GAME_CREATION_AND_GAMEPLAY_PRD_2026-04-22.md new file mode 100644 index 00000000..1c850c3b --- /dev/null +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_BIG_FISH_GAME_CREATION_AND_GAMEPLAY_PRD_2026-04-22.md @@ -0,0 +1,1180 @@ +# AI 原生 Agent-First 大鱼吃小鱼玩法创作工具与玩法系统 PRD + +更新时间:`2026-04-22` + +## 0. 文档目的 + +这份 PRD 用于在当前平台内新增一条完整可落地的“大鱼吃小鱼类玩法”产品主链。 + +本次不是只补一个玩法原型,也不是只补一个玩法模板,而是要把下面这整条链路一次设计到可直接编码: + +1. 平台内新增该玩法的创作入口 +2. Agent 聊天收集高杠杆锚点 +3. 基于锚点生成结果页 +4. 在结果页里生成各等级实体主图 +5. 在结果页里生成各等级实体动作 +6. 在结果页里生成整个活动区域背景图 +7. 发布为可运行玩法 +8. 进入竖屏全屏实时玩法运行态 + +本稿必须满足两个硬要求: + +1. 不能沿用 RPG 创作链里的旧命名和旧数据口径,把实时吞噬玩法硬塞进 `rpg` 或旧 `customWorld` 语义里。 +2. 每个模块必须写到能直接指导前端、`server-rs` Rust 后端、资产生成链和运行时模拟落地,不能停留在概念描述层。 + +--- + +## 1. 一句话定义 + +大鱼吃小鱼玩法是一个 `Agent-First` 的轻量实时成长玩法创作链: + +**创作者先与 Agent 共创“进化母题、等级阶梯、生态风格与场地气质”,系统再自动编译出一个竖屏全屏、摇杆移动、吞噬收编、三合一进化、持续刷怪、升到最高级即通关的可运行玩法作品。** + +--- + +## 2. 本次目标 + +本次迭代必须同时满足下面这些目标: + +1. 平台支持新建“大鱼吃小鱼类玩法”作品。 +2. 创作入口仍然先走 Agent 聊天,而不是直接堆一屏表单。 +3. Agent 聊天阶段必须围绕高杠杆锚点收集,不做低价值逐项盘问。 +4. 锚点稳定后,系统必须生成独立结果页,而不是直接发布或直接进入游戏。 +5. 结果页至少支持: + - 各等级实体主图 AI 生成 + - 各等级实体动作 AI 生成 + - 整个游戏活动区域背景图 AI 生成 +6. 运行态必须是手机竖屏优先、全屏可玩的实时玩法。 +7. 玩家开局从最低等级实体开始。 +8. 玩家通过摇杆向上下左右移动。 +9. 玩家吞噬更低等级实体后,不是只加分,而是把它纳入己方控制队列。 +10. 任意 `3` 个同等级己方实体必须自动合成为更高一级实体。 +11. 场地必须持续生成与玩家当前等级相邻的威胁与猎物。 +12. 相对玩家当前等级已经失去价值的野生实体,必须按屏外 `3` 秒规则回收。 +13. 玩家拥有最高等级实体后立即通关。 +14. 所有真实玩法规则、实体生成、碰撞结算、合成和清理都由 `server-rs` 后端负责,前端只负责表现、输入和渲染。 +15. 整个链路默认复用现有平台创作入口、作品管理和结果工作台思路,不新建脱离平台的独立系统。 + +--- + +## 3. 明确不做 + +本次 PRD 明确不做下面这些内容: + +1. 不做 PvP。 +2. 不做联机同屏。 +3. 不做技能树、装备树、局外养成。 +4. 不做多地图章节切换。 +5. 不做横屏。 +6. 不做前端本地模拟真相源。 +7. 不把玩法规则说明长文默认写进 UI 面板。 +8. 不把“生成主图 / 生成动作 / 生成背景”做成当前卡片下方展开大段内容,统一走独立面板或独立工坊弹层。 +9. 不把大鱼吃小鱼玩法强行接入 RPG 的 `stateFunctions.ts` 或 RPG runtime story function 池。 +10. 不做无限泛化的“任意街机玩法统一引擎”。 +11. 不要求本期支持多种胜利条件并行发布。 +12. 不要求本期支持复杂物理浮力、技能释放、机关障碍和 Boss 战。 + +一句话边界: + +**先把“创作可控 + 资产可生 + 实时可玩”的大鱼吃小鱼主链做成,再讨论更大范围的街机玩法抽象。** + +--- + +## 4. 为什么不能直接复用 RPG 创作链 + +虽然当前平台已经有较成熟的 `Agent-First` RPG 创作链,但这类玩法与 RPG 在 4 个核心层面完全不同: + +1. 创作对象不同 + - RPG 的核心对象是世界、角色、场景、章节。 + - 大鱼吃小鱼的核心对象是等级阶梯、实体形象、动作、场地、实时刷怪规则。 + +2. 结果页工作台不同 + - RPG 结果页围绕世界底稿、角色和场景精修。 + - 大鱼吃小鱼结果页围绕等级实体资产、场地资产和运行参数精修。 + +3. 运行态不同 + - RPG 主要是叙事驱动、回合/单步推进。 + - 大鱼吃小鱼是连续输入、连续碰撞、连续刷怪、连续合成的实时玩法。 + +4. 后端职责不同 + - RPG 后端重心是结构化世界编译、剧情推进和状态裁决。 + - 大鱼吃小鱼后端重心是实时模拟、刷怪、碰撞、合成、实体清理和 run snapshot。 + +因此本次必须新增独立玩法域,而不是继续把所有东西都落在 `rpgCreation`、`rpgRuntime` 或旧 `customWorld` 语义里。 + +--- + +## 5. 产品定位 + +## 5.1 产品名 + +建议正式命名: + +`Agent-First 大鱼吃小鱼玩法创作工具` + +玩法运行态对外展示名可由创作者自定义,不强绑平台内部域名。 + +## 5.2 目标用户 + +目标用户主要是 3 类: + +1. 轻创作者 + - 想快速做一个可玩的成长吞噬小游戏,但不懂完整关卡编辑器 + +2. 视觉驱动型创作者 + - 更关心“每级长什么样、动作怎么样、背景氛围如何” + +3. 玩法原型创作者 + - 想快速验证一套吞噬成长节奏、等级曲线和场地压迫感 + +## 5.3 成功标准 + +本期上线后,至少要满足下面这些结果: + +1. 创作者可在 `5~12` 分钟内通过 Agent 聊天形成一版可用锚点草稿。 +2. 系统默认能编译出 `8` 级实体阶梯的初版玩法草稿。 +3. 每一级实体都能在结果页单独生成和重生成主图。 +4. 每一级实体都能在结果页单独生成和重生成动作。 +5. 活动区域背景能在结果页单独生成和重生成。 +6. 运行态能稳定完成: + - 开局 + - 吞噬收编 + - 三合一升级 + - 高等级敌方吞噬己方实体 + - 屏外清理 + - 达到最高级通关 +7. 前端不承担规则真相源。 +8. 移动端竖屏下一屏内能完成观察、移动和局内反馈。 + +--- + +## 6. 用户主链 + +新增玩法后的平台主链必须是: + +```text +平台创作入口 +-> 选择“大鱼吃小鱼类玩法” +-> 进入 Agent 创作会话 +-> 聊天收集高杠杆锚点 +-> 后端编译玩法草稿 +-> 进入结果页工作台 +-> 逐级生成主图 / 动作 +-> 生成活动区域背景图 +-> 调整玩法参数 +-> 发布玩法 +-> 进入竖屏实时运行态 +``` + +这条链路必须与当前 RPG 创作入口并列存在,但不能共用错误的业务名。 + +--- + +## 7. Agent 聊天阶段设计 + +## 7.1 Agent 聊天的职责 + +Agent 在这一玩法里不负责实时玩法裁决,它只负责 3 件事: + +1. 帮创作者明确高杠杆锚点 +2. 帮创作者把模糊灵感总结成可编译结构 +3. 帮创作者收束出第一版等级阶梯与视觉方向 + +## 7.2 前台交互原则 + +前台必须继续遵守当前 `Agent-First` 创作链已经验证有效的原则: + +1. 不做固定多题问卷 +2. 每轮只问 `1` 个主问题 +3. 会总结,不只会追问 +4. 会补缺,不会平均盘问 +5. 进度基于真实锚点完成度,而不是机械轮次 + +## 7.3 大鱼吃小鱼玩法的 4 个最小高杠杆锚点 + +这条玩法链路不再采用过多锚点,而是收束为下面 `4` 个最小高杠杆锚点。 + +原因很简单: + +1. 这类玩法比 RPG 创作更轻,锚点过多会把用户重新拉回填表状态。 +2. 真正会显著影响第一版玩法草稿质量的,只有爽点、母题、成长阶梯和风险节奏这 `4` 件事。 +3. 其余信息应由系统默认、由等级蓝图编译推导,或并入更高层锚点,而不是继续单列追问。 + +这条玩法链路采用下面 `4` 个锚点: + +1. `玩法承诺` + - 这局游戏最想让玩家爽到什么 + - 例如:弱小逆袭、群体吞并、诡异进化、可爱生态成长 + +2. `生态与视觉母题` + - 这套实体体系是什么题材、世界观气质与整体视觉方向 + - 它同时决定实体主图方向和活动区域背景方向 + - 例如:深海生物、机械微生物、梦境纸鱼、像素远古水域 + +3. `成长阶梯` + - 这一玩法一共大致有几级,以及每一级如何逐步升级、变大、变强、变异 + - 最高级终局形态也并入这一锚点统一确定 + - 若创作者没有明确总层数,本期默认按 `8` 级编译,可配置范围固定为 `6~12` 级 + +4. `风险节奏` + - 玩家周围应该更偏压迫、平衡还是偏爽快 + +## 7.3.1 不再单列的旧锚点去向 + +为了避免后续实现偷偷把锚点数量再长回去,本期明确把旧的细分锚点并入下面这些大锚点: + +1. `场地气质` 并入 `生态与视觉母题` +2. `等级总层数` 并入 `成长阶梯` +3. `升级轮廓` 并入 `成长阶梯` +4. `终局形态` 并入 `成长阶梯` +5. `开局成长方式` 改为系统固定规则,不再作为创作者锚点 + +后续 Agent 追问时,不再把这些内容拆成独立必答题。 + +## 7.4 锚点状态 + +每个锚点都必须支持 4 种状态: + +1. `已确认` +2. `Agent 推断` +3. `待补充` +4. `已锁定` + +## 7.5 Agent 的收束条件 + +满足下面条件后,系统才允许进入玩法草稿编译: + +1. `玩法承诺` 已确认或已锁定 +2. `生态与视觉母题` 已确认或已锁定 +3. `成长阶梯` 至少具备一版 Agent 草稿,其中总层数可走默认值 +4. `风险节奏` 已确认,或由系统落默认 `平衡` 值 + +换句话说: + +**只要玩法爽点、生态母题、成长阶梯和风险节奏已经够清晰,系统就应允许直接进入玩法草稿编译。** + +--- + +## 8. 开局成长死锁的正式产品结论 + +用户已经明确提出了一个必须正面解决的规则死角: + +**玩家从最低等级开局,如果只能吃比自己更低等级的实体,就会造成无法成长。** + +本次 PRD 在产品层直接定死下面这条规则,不允许后续实现继续含糊处理: + +## 8.1 固定吞噬规则 + +玩家控制的己方实体在碰撞到野生实体时,统一采用下面这条固定规则: + +1. 如果野生实体等级小于己方实体等级,则可吞噬收编 +2. 如果野生实体等级等于己方实体等级,则也可吞噬收编 +3. 如果野生实体等级高于己方实体等级,则己方实体被吃掉 + +这条规则从开局到通关全程生效,不存在只对 `1` 级开放的临时特例。 + +## 8.2 开局保障刷怪 + +为了避免玩家第一局一上来就看不到可吸收对象,系统还必须同时满足: + +1. 开局前 `3` 秒内,玩家视野内至少刷出 `2` 个与玩家同级的野生实体 +2. 这两个实体必须位于玩家可达范围内,不能刷在屏幕过远边缘 + +## 8.3 规则意义 + +这条固定规则的意义是: + +1. 最低等级开局不会死锁 +2. 玩家从第一秒开始就能理解同一套吞噬关系 +3. 后端不需要维护一套易错的“只在开局生效的特例状态” + +--- + +## 9. 第一版玩法草稿编译规则 + +## 9.1 默认草稿规模 + +当创作者没有特别指定时,第一版玩法草稿必须默认编译为: + +1. `8` 级实体阶梯 +2. `1` 张活动区域背景图 +3. 每级 `1` 个核心实体形象定义 +4. 每级 `2` 个基础动作位 +5. `1` 套运行参数草稿 + +## 9.2 每级实体蓝图必须包含的字段 + +后端在生成第一版草稿时,每一级 `level blueprint` 至少要包含: + +1. `level` + - 等级序号,从 `1` 开始连续递增 + +2. `name` + - 当前级实体名称 + +3. `oneLineFantasy` + - 这一等级的核心感觉一句话 + +4. `silhouetteDirection` + - 外轮廓方向 + +5. `sizeRatio` + - 相对于 `1` 级实体的大致尺寸比例 + +6. `visualPromptSeed` + - 主图生成提示词种子 + +7. `motionPromptSeed` + - 动作生成提示词种子 + +8. `mergeSourceLevel` + - 由哪一级三合一而来 + +9. `preyWindow` + - 推荐猎物等级区间 + +10. `threatWindow` + - 推荐威胁等级区间 + +11. `isFinalLevel` + - 是否为最高等级 + +## 9.3 活动区域蓝图必须包含的字段 + +活动区域背景蓝图至少包含: + +1. `theme` +2. `colorMood` +3. `foregroundHints` +4. `midgroundComposition` +5. `backgroundDepth` +6. `safePlayAreaHint` +7. `spawnEdgeHint` +8. `backgroundPromptSeed` + +## 9.4 运行参数草稿必须包含的字段 + +运行参数草稿至少包含: + +1. `levelCount` +2. `mergeCountPerUpgrade` +3. `spawnTargetCount` +4. `leaderMoveSpeed` +5. `followerCatchUpSpeed` +6. `offscreenCullSeconds` +7. `preySpawnDeltaLevels` +8. `threatSpawnDeltaLevels` +9. `winLevel` + +其中: + +1. `mergeCountPerUpgrade` 本期固定为 `3` +2. `offscreenCullSeconds` 本期固定为 `3` +3. `preySpawnDeltaLevels` 本期固定为 `[1, 2]` +4. `threatSpawnDeltaLevels` 本期固定为 `[1, 2]` + +--- + +## 10. 结果页工作台设计 + +## 10.1 结果页职责 + +大鱼吃小鱼玩法结果页不是资料总表,它是一个围绕“等级实体资产 + 场地资产 + 运行参数”组织的工作台。 + +## 10.2 结果页必须包含的模块 + +结果页至少必须包含下面 `6` 个模块: + +1. `玩法摘要区` + - 显示玩法名、副标题、核心爽点、生态母题、等级总数 + +2. `等级阶梯区` + - 按等级展示所有实体蓝图 + +3. `实体主图区` + - 为每一级生成和预览主图 + +4. `实体动作区` + - 为每一级生成和预览动作 + +5. `场地区` + - 生成和预览整个活动区域背景图 + +6. `发布校验区` + - 显示是否达到可发布状态 + +## 10.3 移动端优先布局要求 + +结果页必须优先保证竖屏移动端成立: + +1. 首屏先看见玩法摘要和当前焦点等级卡 +2. 各等级卡纵向排列或纵向焦点滑动 +3. 主图、动作、背景入口使用简洁按钮 +4. 点击按钮统一弹出独立面板 +5. 不在当前卡片下方展开长段规则说明 + +## 10.4 等级卡需要呈现的信息 + +每张等级卡至少展示: + +1. 等级序号 +2. 实体名 +3. 一句话定位 +4. 当前主图缩略图 +5. 当前动作状态 +6. 当前等级的猎物窗口 +7. 当前等级的威胁窗口 + +默认不展示大段规则描述文字。 + +## 10.5 结果页允许的核心操作 + +结果页必须支持下面这些动作: + +1. `重新生成本级设定` +2. `生成本级主图` +3. `重新生成本级主图` +4. `生成本级动作` +5. `重新生成本级动作` +6. `生成场地背景` +7. `重新生成场地背景` +8. `进入测试玩法` +9. `发布玩法` + +--- + +## 11. 主图生成设计 + +## 11.1 主图生成范围 + +每一级实体都必须支持独立主图生成。 + +## 11.2 主图生成的最小产物 + +每次主图生成至少产出: + +1. `mainImage` + - 当前等级的主图 + +2. `thumbnail` + - 结果页和列表使用的缩略图 + +3. `promptSnapshot` + - 本次生成时冻结的提示词快照 + +## 11.3 主图生成的交互方式 + +主图生成必须在独立资产面板中完成,面板里至少要有: + +1. 当前等级设定摘要 +2. 生成提示词摘要 +3. 候选图预览 +4. 设为正式主图 +5. 重新生成 + +## 11.4 主图发布校验 + +一个等级在发布前,必须恰好有 `1` 张被标记为正式的主图资产。 + +--- + +## 12. 动作生成设计 + +## 12.1 动作生成的产品结论 + +用户要求“各个等级实体形象进行 AI 生成动作的功能”,本期必须正式支持。 + +但为了保证首版可落地,动作生成范围本期先冻结为每级 `2` 个基础动作位: + +1. `idle_float` + - 原地漂浮 / 游动待机 + +2. `move_swim` + - 正常移动游动 + +## 12.2 本期不强制逐级独立生成的动作 + +下面这些动作本期不作为发布硬门槛: + +1. `eat_bite` +2. `merge_flash` +3. `hit_react` +4. `victory_pose` + +这些可以在结构上预留,但不要求首版发布必须逐级全部产出。 + +## 12.3 动作生成面板必须支持 + +每级动作生成面板至少包含: + +1. 当前等级主图参考 +2. 当前动作位选择 +3. 动作提示词摘要 +4. 候选动作预览 +5. 设为正式动作 +6. 重新生成动作 + +## 12.4 动作发布校验 + +发布前每一级实体必须至少拥有: + +1. `idle_float` 正式动作 +2. `move_swim` 正式动作 + +--- + +## 13. 场地背景生成设计 + +## 13.1 产物要求 + +活动区域背景图必须支持 AI 生成,且首版只要求 `1` 张主背景图。 + +## 13.2 背景图的固定要求 + +背景图必须满足: + +1. 竖屏 `9:16` +2. 适配全屏运行态 +3. 中央主体活动区域视觉清晰 +4. 画面边缘允许作为实体出生区的视觉缓冲 +5. 不带默认文字、规则说明或 UI 框 + +## 13.3 背景生成面板 + +背景生成必须在独立面板中完成,面板至少支持: + +1. 当前场地气质摘要 +2. 背景提示词摘要 +3. 候选图预览 +4. 设为正式背景 +5. 重新生成背景 + +## 13.4 背景发布校验 + +发布前必须至少存在 `1` 张正式活动区域背景图。 + +--- + +## 14. 运行态总体形态 + +## 14.1 一句话定义 + +运行态是一个竖屏全屏的实时成长吞噬玩法页面: + +1. 摄像机以当前主控实体为中心 +2. 玩家通过虚拟摇杆持续移动 +3. 周围不断刷出高于或低于玩家当前等级的野生实体 +4. 吞噬低于或等于自己等级的野生实体并纳入己方 +5. 任意 `3` 个同等级己方自动合成更高一级 +6. 己方实体被高等级野生实体碰到时会被吃掉 +7. 达到最高等级后立刻通关 + +## 14.2 运行态首屏要素 + +运行态首屏至少包含: + +1. 活动区域背景 +2. 玩家当前主控实体 +3. 己方跟随实体 +4. 野生实体 +5. 当前最高等级提示 +6. 左下或右下的虚拟摇杆 +7. 简洁的暂停或退出入口 + +不默认堆规则说明文案。 + +--- + +## 15. 实时玩法规则 + +## 15.1 实体分类 + +运行时实体只有下面 3 类: + +1. `leader` + - 玩家当前直接控制的主控实体 + +2. `ownedFollower` + - 已归属玩家、跟随主控移动的己方实体 + +3. `wildEntity` + - 场地中的野生实体 + +## 15.2 主控实体规则 + +主控实体的定义固定为: + +1. 优先取己方最高等级实体 +2. 若最高等级实体有多个,取距离当前镜头中心最近的一个 +3. 若当前主控实体被吃掉,系统必须立即重选新的主控实体 +4. 若己方实体数量归零,则本局失败 + +## 15.3 移动规则 + +玩家通过二维虚拟摇杆输入一个归一化移动向量: + +1. `x` + - 左右 + +2. `y` + - 上下 + +后端根据该向量驱动主控实体位移,前端不本地决定最终位置。 + +## 15.4 跟随规则 + +己方跟随实体必须跟随主控实体移动。 + +第一版跟随逻辑冻结为: + +1. 主控实体走真实位置 +2. 跟随实体按后端分配的跟随槽位追赶 +3. 不要求前端自己计算编队真相 + +## 15.5 吞噬收编规则 + +当己方任意实体碰到野生实体时,按下面规则裁决: + +1. 如果己方等级大于野生实体等级,则该野生实体被吞噬收编 +2. 如果己方等级等于野生实体等级,则该野生实体也被吞噬收编 +3. 被吞噬后,该实体转为己方实体 +4. 转为己方后,它按自己原本等级进入己方编队 +5. 收编不会直接跳级,升级只能通过三合一完成 + +## 15.6 同级碰撞规则 + +同级碰撞默认发生吞噬收编: + +1. 如果碰撞双方为“己方实体 + 野生实体”且等级相同,则野生实体被己方收编 +2. 如果碰撞双方同为己方实体,则只做队形避让,不发生吞噬 +3. 如果碰撞双方同为野生实体,则只做轻微错位,不互相吞噬 + +## 15.7 高等级威胁规则 + +如果野生实体等级高于己方被碰撞实体等级,则: + +1. 己方该实体立刻被吃掉 +2. 被吃掉的己方实体从己方队列移除 +3. 如果被吃掉的是主控实体,则立即重选主控实体 +4. 如果移除后己方数量为 `0`,则本局失败 + +## 15.8 三合一升级规则 + +己方实体满足下面条件时必须自动合成: + +1. 任意 `3` 个同等级己方实体 +2. 这 `3` 个实体立刻进入合成队列 +3. 合成后生成 `1` 个高一级己方实体 +4. 原 `3` 个实体移除 + +## 15.9 连锁合成规则 + +合成必须支持连锁: + +1. 先从低等级开始检查 +2. 每次合成完成后重新检查全队列 +3. 直到不存在可继续三合一的等级为止 + +例如: + +1. 新收编 `1` 个 `1` 级实体 +2. 导致 `3` 个 `1` 级合成 `1` 个 `2` 级 +3. 如果此时 `2` 级也达到 `3` 个 +4. 则继续合成 `3` 级 + +## 15.10 通关规则 + +当玩家第一次拥有最高等级己方实体时,立即进入通关状态。 + +本期通关裁决固定为: + +1. 最高等级实体生成成功 +2. 该实体成为当前最高等级己方实体 +3. 系统立刻结算胜利 + +不要求继续拖延到额外击杀或额外倒计时。 + +--- + +## 16. 场地持续生成规则 + +## 16.1 当前等级基准 + +刷怪与清理都以“玩家当前最高己方等级”作为基准等级,记为 `playerLevel`。 + +## 16.2 正常刷怪区间 + +场地持续生成的野生实体只围绕下面 4 个差值带展开: + +1. `playerLevel - 1` +2. `playerLevel - 2` +3. `playerLevel + 1` +4. `playerLevel + 2` + +超出这个区间的实体不作为常规刷怪目标。 + +## 16.3 开局特例刷怪 + +当 `playerLevel = 1` 时: + +1. `-1` 级与 `-2` 级不存在 +2. 系统将“低等级带”替换为 `1` 级普通野生实体 +3. 这些同级野生实体可直接被玩家收编 + +## 16.4 刷怪总量目标 + +为了让节奏稳定,后端必须维持一个野生实体目标池。 + +第一版建议固定为: + +1. 同时存在的野生实体目标数为 `12` +2. 其中低等级带优先维持 `7` +3. 高等级带优先维持 `5` + +如果后续调优需要修改,也必须由后端配置驱动,前端不做本地推断。 + +## 16.5 出生点要求 + +野生实体生成位置必须满足: + +1. 默认出生在当前可视区外缘附近 +2. 不直接贴脸刷在主控实体正中心 +3. 开局保障实体允许出现在视野内,但不得直接与主控实体重叠 + +--- + +## 17. 屏外删除与回收规则 + +用户已经明确提出下面这条规则,本期必须按产品层直接写死: + +**当与玩家同等级、高两级以上、低两级以下的实体,在玩家屏幕外 `3` 秒后必须删除。** + +为避免工程实现歧义,本期正式定义如下: + +## 17.1 回收对象只针对野生实体 + +屏外自动删除只作用于 `wildEntity`。 + +己方实体不进入这套删除规则。 + +## 17.2 必删条件 + +若野生实体满足下面任意一条,并且连续处于屏幕外 `3` 秒,则必须删除: + +1. 实体等级等于当前 `playerLevel` +2. 实体等级大于等于 `playerLevel + 3` +3. 实体等级小于等于 `playerLevel - 3` + +## 17.3 “屏幕外”的定义 + +当实体完整包围盒离开当前摄像机可视区域后,才开始计算屏外计时。 + +## 17.4 计时重置条件 + +在计时期间,如果实体重新进入可视区域,则: + +1. 屏外计时立即清零 +2. 下次再离开时重新计时 `3` 秒 + +## 17.5 规则意义 + +这条规则的作用是: + +1. 清理因玩家升级导致已失去交互价值的旧实体 +2. 避免同级和超远等级实体长期堆积 +3. 保持刷怪池始终围绕玩家当前成长段位 + +--- + +## 18. 失败规则 + +本期失败条件固定为: + +1. 己方实体总数降为 `0` + +只要还剩至少 `1` 个己方实体,就不进入失败。 + +不额外引入时间耗尽、受伤值累计或目标数未达成等失败条件。 + +--- + +## 19. 前后端职责边界与技术栈 + +## 19.1 前端职责 + +前端只负责: + +1. 创作入口表现 +2. Agent 聊天输入与展示 +3. 结果页工作台展示 +4. 主图 / 动作 / 背景生成面板展示 +5. 运行态渲染 +6. 摇杆输入采集 +7. 后端快照播放与局内反馈表现 + +前端技术栈约束固定为: + +1. Web / H5 前端继续使用现有 TypeScript 前端工程 +2. 前端默认只消费 `api-server` 暴露的 HTTP / SSE / WebSocket facade +3. 若后续接入 SpacetimeDB TypeScript 绑定,也只允许读取订阅态,不允许在前端本地自建第二套玩法真相 + +## 19.2 Rust 后端职责 + +`server-rs` 后端必须负责: + +1. 锚点抽取与状态维护 +2. 玩法草稿编译 +3. 等级蓝图编译 +4. 主图 / 动作 / 背景资产任务调度 +5. 发布校验 +6. 实时 run 创建 +7. 输入向量消费 +8. 实体移动模拟 +9. 刷怪 +10. 碰撞裁决 +11. 收编 +12. 三合一合成 +13. 屏外删除 +14. 胜负裁决 +15. run snapshot 存储 + +后端技术栈约束固定为: + +1. `server-rs/crates/api-server` + - 作为唯一 HTTP、SSE、WebSocket 边界层,基于 `Axum` 提供 facade +2. `server-rs/crates/spacetime-module` + - 作为玩法真相表、reducer、procedure 的主承载层 +3. `server-rs/crates/spacetime-client` + - 作为 Rust 应用层调用 SpacetimeDB 的统一 facade +4. `server-rs/crates/shared-contracts` + - 作为前后端公开 DTO 与返回 contract 的统一来源 +5. `OSS` + - 作为主图、动作、背景图等二进制资产的唯一对象存储 + +明确禁止继续把本玩法主链实现为: + +1. `server-node` +2. `Express` +3. `PostgreSQL` + +## 19.3 SpacetimeDB 边界 + +涉及本玩法的实时状态、成长规则和实体同步,统一遵守下面这些 SpacetimeDB 约束: + +1. 表是真相源 +2. reducer / procedure 是唯一状态写入口 +3. 查询与订阅负责读模型,不依赖 reducer 返回业务数据 +4. 身份校验依赖可信上下文,不信任前端显式传入 `userId` + +## 19.3 明确禁止前端做的事 + +前端明确禁止做下面这些事: + +1. 本地决定哪些实体该刷 +2. 本地决定谁吃掉谁 +3. 本地决定何时合成 +4. 本地决定何时清理屏外实体 +5. 本地把多个实体改造成一套“看起来像对的”假状态 + +--- + +## 20. 运行时通信结论 + +因为这是实时玩法,本期通信方式必须与 RPG 的一次一动作请求不同。 + +本期正式结论如下: + +1. 运行态采用长连接实时通道 +2. 前端持续发送归一化移动向量 +3. `api-server` 负责承接实时连接,并把输入编排到运行时主链 +4. 后端按固定 tick 推进模拟 +5. 后端持续下发 runtime snapshot 或订阅态更新 + +推荐落地方式: + +1. `api-server` 上的 WebSocket 优先 +2. 若兼容期需要过渡,才允许保留有限 HTTP 辅助接口 +3. 稳定阶段可评估前端通过 SpacetimeDB TypeScript 绑定直接订阅 run state,但当前主链默认仍以 `Axum` facade 为准 + +但无论采用什么协议: + +**输入、模拟、快照下发必须围绕“后端是真相源”这一原则设计。** + +--- + +## 21. 数据结构建议 + +为了避免后续编码命名漂移,本次先给出建议的数据对象命名。 + +## 21.1 创作态对象 + +1. `BigFishAnchorPack` +2. `BigFishCreationSession` +3. `BigFishCreationWorkSummary` +4. `BigFishDraftCompileResult` + +## 21.2 结果页对象 + +1. `BigFishGameDraft` +2. `BigFishLevelBlueprint` +3. `BigFishBackgroundBlueprint` +4. `BigFishAssetCoverage` +5. `BigFishPublishCheckResult` + +## 21.3 运行态对象 + +1. `BigFishRunState` +2. `BigFishRuntimeSnapshot` +3. `BigFishOwnedEntityState` +4. `BigFishWildEntityState` +5. `BigFishSpawnDirectorState` +6. `BigFishCollisionResult` + +## 21.4 Rust crate 落位建议 + +为了避免后续实现又把新玩法写回旧栈,本玩法建议按下面这套 Rust 工作区结构落位: + +1. `server-rs/crates/api-server` + - Big Fish 创作、资产、运行态的 HTTP / SSE / WebSocket facade +2. `server-rs/crates/spacetime-client` + - Big Fish 调用 facade 与 record 映射 +3. `server-rs/crates/spacetime-module` + - Big Fish 表、reducer、procedure +4. `server-rs/crates/shared-contracts` + - Big Fish 对外 contract +5. `src/services/big-fish-*` + - 前端 client、adapter、view model + +--- + +## 22. 平台内脚本命名规范 + +这次新增玩法域后,必须显式遵守平台内脚本命名规范。 + +核心原则只有一句话: + +**这是一个新的“大鱼吃小鱼玩法域”,不能继续沿用 `rpg*`、`customWorld*` 或过于泛化的 `runtime*` 命名。** + +## 22.1 命名根 + +后续新增模块统一使用下面 4 组命名根: + +1. `bigFishCreation` + - 创作入口、聊天、结果页工作台 + +2. `bigFishGame` + - 玩法作品草稿、预览、发布态 + +3. `bigFishRuntime` + - 实时运行态、快照、输入、模拟 + +4. `bigFishAssets` + - 主图、动作、背景图资产生成 + +## 22.2 前端文件命名规则 + +1. React 组件统一使用 `BigFish...` 前缀 +2. hooks 统一使用 `useBigFish...` 前缀 +3. client / gateway / adapter 统一使用 `bigFish...` 小驼峰前缀 +4. 目录命名建议显式分域,不允许再混进 RPG 目录 + +### 前端目录示例 + +1. `src/components/big-fish-creation/` +2. `src/components/big-fish-result/` +3. `src/components/big-fish-runtime/` +4. `src/components/big-fish-assets/` +5. `src/services/big-fish-creation/` +6. `src/services/big-fish-runtime/` +7. `src/services/big-fish-assets/` + +### 前端文件示例 + +1. `src/components/big-fish-creation/BigFishCreationShell.tsx` +2. `src/components/big-fish-creation/BigFishAgentWorkspace.tsx` +3. `src/components/big-fish-result/BigFishResultView.tsx` +4. `src/components/big-fish-result/BigFishLevelCard.tsx` +5. `src/components/big-fish-assets/BigFishLevelImageStudioModal.tsx` +6. `src/components/big-fish-assets/BigFishLevelMotionStudioModal.tsx` +7. `src/components/big-fish-assets/BigFishBackgroundStudioModal.tsx` +8. `src/components/big-fish-runtime/BigFishRuntimeShell.tsx` +9. `src/hooks/big-fish-runtime/useBigFishRuntimeSession.ts` +10. `src/services/big-fish-runtime/bigFishRuntimeClient.ts` + +## 22.3 Rust 后端文件命名规则 + +1. `api-server` 中的 handler / facade 文件使用 `big_fish_*` 或显式 `BigFish...` 命名 +2. Rust 应用服务使用 `BigFish...Service.rs` +3. 编译器使用 `BigFish...Compiler.rs` +4. `spacetime-client` facade 使用 `big_fish_*` 或 `BigFish...Client.rs` +5. `spacetime-module` 中的表、reducer、procedure 使用显式业务名,不用 `helper`、`manager` + +### Rust 后端目录示例 + +1. `server-rs/crates/api-server/src/big_fish_creation.rs` +2. `server-rs/crates/api-server/src/big_fish_runtime.rs` +3. `server-rs/crates/api-server/src/big_fish_assets.rs` +4. `server-rs/crates/spacetime-client/src/big_fish/` +5. `server-rs/crates/spacetime-module/src/big_fish/` +6. `server-rs/crates/shared-contracts/src/` + +### Rust 后端文件示例 + +1. `server-rs/crates/api-server/src/big_fish_creation.rs` +2. `server-rs/crates/api-server/src/big_fish_runtime.rs` +3. `server-rs/crates/api-server/src/big_fish_assets.rs` +4. `server-rs/crates/spacetime-client/src/big_fish/big_fish_creation_client.rs` +5. `server-rs/crates/spacetime-client/src/big_fish/big_fish_runtime_client.rs` +6. `server-rs/crates/spacetime-client/src/big_fish/big_fish_asset_client.rs` +7. `server-rs/crates/spacetime-module/src/big_fish/big_fish_creation.rs` +8. `server-rs/crates/spacetime-module/src/big_fish/big_fish_runtime.rs` +9. `server-rs/crates/spacetime-module/src/big_fish/big_fish_assets.rs` +10. `server-rs/crates/shared-contracts/src/big_fish_creation.rs` +11. `server-rs/crates/shared-contracts/src/big_fish_assets.rs` +12. `server-rs/crates/shared-contracts/src/big_fish_runtime.rs` + +## 22.4 共享契约命名规则 + +共享契约建议命名为: + +1. `server-rs/crates/shared-contracts/src/big_fish_creation.rs` +2. `server-rs/crates/shared-contracts/src/big_fish_assets.rs` +3. `server-rs/crates/shared-contracts/src/big_fish_runtime.rs` +4. `server-rs/crates/shared-contracts/src/big_fish_game_profile.rs` + +前端 TypeScript 如需镜像类型,必须以 Rust contract 为准同步生成或显式对齐,不允许前后端各自维护一套漂移字段。 + +## 22.5 动作 / 任务 / 指令 ID 命名规则 + +如果平台内需要为结果页按钮、后端任务或实时指令定义显式 ID,统一使用 `big_fish_` 前缀的蛇形命名。 + +推荐示例: + +1. `big_fish_generate_level_main_image` +2. `big_fish_generate_level_motion` +3. `big_fish_generate_stage_background` +4. `big_fish_regenerate_level_blueprint` +5. `big_fish_start_test_run` +6. `big_fish_publish_game` +7. `big_fish_move_vector` +8. `big_fish_pause_run` +9. `big_fish_resume_run` +10. `big_fish_restart_run` + +## 22.6 命名禁忌 + +后续落地时禁止继续新增下面这些错误命名: + +1. `customWorld*` + - 这是旧世界创作语义,不适合当前玩法域 + +2. `rpg*` + - 这是 RPG 专属域名 + +3. `runtimeRoutes.ts` + - 过宽单文件总入口命名 + +4. `manager.ts` + - 过于泛化 + +5. `helper.ts` + - 不能作为主业务模块名 + +6. `flowCoordinator.ts` + - 层级含义不清 + +--- + +## 23. 发布门槛 + +玩法作品只有在满足下面全部条件后,才允许发布: + +1. 锚点达到最小完整度 +2. 等级总层数合法 +3. 每一级都存在正式主图 +4. 每一级都存在正式 `idle_float` +5. 每一级都存在正式 `move_swim` +6. 存在正式活动区域背景图 +7. 运行参数校验通过 +8. Rust 后端 dry-run 模拟通过 + +## 23.1 dry-run 模拟必须验证的内容 + +发布前 `server-rs` 后端至少要验证下面这些玩法闭环: + +1. 开局能正常刷出可成长目标 +2. 同级吞噬收编可发生 +3. 低等级吞噬收编可发生 +4. 三合一合成可发生 +5. 高等级威胁可吃掉己方实体 +6. 屏外 `3` 秒删除规则可触发 +7. 最高等级通关可触发 + +--- + +## 24. 分阶段落地建议 + +为了降低风险,建议按下面 `3` 个阶段推进: + +## 24.1 Phase 1:创作链与结果页骨架 + +本阶段只完成: + +1. 新玩法入口 +2. Agent 聊天收集锚点 +3. Rust 后端玩法草稿编译 +4. 结果页骨架 +5. 等级卡与背景卡展示 + +本阶段不要求运行态可玩。 + +## 24.2 Phase 2:资产工坊 + +本阶段完成: + +1. 各等级主图生成 +2. 各等级动作生成 +3. 场地背景生成 +4. 发布校验 + +## 24.3 Phase 3:实时玩法 MVP + +本阶段完成: + +1. 竖屏运行态 +2. 摇杆输入 +3. Rust 后端实时模拟 +4. 刷怪 +5. 收编 +6. 三合一 +7. 屏外删除 +8. 胜负裁决 + +--- + +## 25. 最终验收口径 + +当下面这些场景全部成立时,本次 PRD 才算真正落地: + +1. 用户可以在平台内选择创建“大鱼吃小鱼类玩法”作品。 +2. 用户创建后会先进入 Agent 会话,而不是直接进入一屏表单。 +3. Agent 会围绕高杠杆锚点推进,不会机械盘问。 +4. 锚点够用后能进入结果页。 +5. 结果页能按等级分别生成主图。 +6. 结果页能按等级分别生成动作。 +7. 结果页能生成整个活动区域背景图。 +8. 用户能进入测试玩法。 +9. 开局从最低级开始时不会卡死。 +10. 玩家可以通过收编和三合一成长。 +11. 高等级野生实体可以吃掉己方实体。 +12. 与玩家同等级、高两级以上、低两级以下的野生实体在屏外 `3` 秒后会被删除。 +13. 玩家获得最高等级实体后立即通关。 +14. 前端没有偷偷承担玩法真相源。 + +--- + +## 26. 一句话结论 + +这次新增的大鱼吃小鱼玩法,不应被实现成“一个带几张图的小游戏模板”,而应被实现成: + +**一个以 Agent 共创高杠杆锚点为起点、以等级实体资产和场地资产工坊为中段、以 `server-rs` 中 `Axum + SpacetimeDB + OSS` 为后端主链真相源的完整新玩法域。** diff --git a/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md new file mode 100644 index 00000000..1b5e3141 --- /dev/null +++ b/docs/prd/AI_NATIVE_PUZZLE_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-22.md @@ -0,0 +1,1109 @@ +# AI 原生拼图玩法创作工具与玩法系统 PRD + +更新时间:`2026-04-22` + +## 0. 文档目的 + +这份 PRD 用于在当前平台内新增一条完整的拼图玩法产品链路,并且明确它与现有 RPG 创作链、作品广场链、结果页链、运行时链之间的关系。 + +本次不是做一个孤立的小游戏页面,也不是只补一个拼图画布组件,而是要一次性定义清楚下面这条完整主链: + +```text +平台创作中心 +-> 选择“拼图玩法” +-> Agent 聊天收束高杠杆锚点 +-> 生成拼图结果页 +-> 创作者生成并确认拼图图片 +-> 发布到拼图广场 +-> 玩家从广场进入第 1 关 +-> 全屏拼图运行时 +-> 通关后按“标签相似度 70% + 同作者 30%”接续下一关 +``` + +这份文档必须达到可以直接指导编码落地的程度,不能只停留在玩法概念说明。 + +--- + +## 1. 一句话定义 + +让创作者通过 Agent 对话确定拼图作品的高杠杆视觉锚点,再由系统生成结果页、AI 生成拼图图片并发布到广场;玩家进入游戏后,在全屏拼图画布中通过交换、合并、拖动和拆分完成关卡,并沿着“相似题材优先、同作者次优先”的关卡链持续游玩。 + +--- + +## 2. 与 RPG 创作流程的关系 + +## 2.1 复用什么 + +拼图玩法不是一条完全独立于平台创作体系的新产品线,而是复用现有平台的 4 个共性骨架: + +1. 创作入口仍然从平台创作中心进入。 +2. 创作主交互仍然是 Agent-first 对话。 +3. 创作结果仍然先进入结果页,再进入发布。 +4. 发布后的作品仍然进入平台广场体系。 + +## 2.2 不复用什么 + +拼图玩法不需要复用 RPG 那套大世界、角色、地点、线程、章节的重型结构。 + +拼图玩法的创作对象更轻,核心只需要收束到: + +1. 关卡题材与视觉承诺 +2. 画面主体与焦点 +3. 视觉风格与气质 +4. 图片题材标签 +5. 禁止事项 + +也就是说: + +**拼图玩法保留 Agent-first 高杠杆锚点流程,但不保留 RPG 的重世界编译链。** + +## 2.3 本次产品判断 + +对于拼图玩法,真正高杠杆的不是世界线程,而是: + +1. 这张图为什么有吸引力 +2. 这张图切开后是否仍然有辨识度 +3. 它应该被归入什么标签语义 +4. 它发布后在广场和后续关卡链里如何被继续消费 + +--- + +## 3. 本次目标 + +本次迭代必须同时满足以下目标: + +1. 平台创作中心新增“拼图玩法”创作入口。 +2. 拼图创作流程必须先经过 Agent 聊天确定高杠杆锚点,再进入结果页。 +3. 拼图结果页至少必须包含: + - 拼图关卡名 + - AI 生成拼图图片的功能 + - 图片题材标签 +4. 创作者发布后的拼图作品必须进入平台广场。 +5. 玩家从广场进入某个作品时,第 1 关必须先显示当前作品本身。 +6. 第 2 关及以后必须按照“标签相似度权重 `70%` + 同作者权重 `30%`”选择下一关。 +7. 游戏运行时必须全屏展示拼图画布。 +8. 新游戏进入时难度必须从 `3*3` 开始,完成 `3` 关后切为 `4*4`,后续持续为 `4*4`。 +9. 拼图运行时必须支持: + - 点击选择两块并交换 + - 正确相邻后自动合并 + - 合并块整体拖动 + - 单块拖到合并块位置时拆分合并块 +10. 游戏画面必须显示作者信息和关卡名。 +11. 前端只负责表现和交互输入,逻辑、数据、关卡裁决、推荐计算、状态存储全部放到 `server-rs` 后端,由 `Axum + SpacetimeDB + OSS` 方案承接。 + +--- + +## 4. 明确不做 + +本次 PRD 明确不做下面这些内容: + +1. 不做旋转拼块。 +2. 不做异形拼块。 +3. 不做时间限制和失败倒计时。 +4. 不做提示系统、道具系统和体力系统。 +5. 不做复杂剧情文本或玩法说明文本默认堆在 UI 中。 +6. 不做独立于平台创作中心之外的新创作站点。 +7. 不做前端本地计算下一关推荐结果。 +8. 不做前端本地裁决拼块合并、拆分和关卡完成。 +9. 不把拼图玩法继续命名挂在 `customWorld` 或 `rpgWorld` 老前缀下。 + +--- + +## 5. 核心产品结论 + +## 5.1 创作端结论 + +拼图玩法的创作端应该是: + +```text +创建拼图作品 +-> Agent 聊天收束 5 个视觉锚点 +-> 生成结果页 +-> 创作者确认关卡名、标签、图片 +-> 发布到拼图广场 +``` + +结果页不是一个只读总结页,而是拼图作品最小可编辑工作台。 + +## 5.2 运行时结论 + +拼图运行时应该是: + +```text +进入关卡 +-> 按网格切图并打乱 +-> 玩家交换两块 +-> 系统只重算受影响区域的正确相邻关系 +-> 正确连接的块自动合并 +-> 合并块可整体拖动 +-> 单块拖到合并块位置可拆分目标合并块 +-> 全部拼块汇成一张图后通关 +``` + +## 5.3 平台分发结论 + +拼图广场不是只承担“浏览作品”,还承担“后续关卡接续池”的作品来源。 + +因此每个拼图作品在发布时,必须具备: + +1. 正式关卡名 +2. 正式拼图图片 +3. 规范化标签 +4. 作者信息 +5. 可进入运行时的正式作品 id + +--- + +## 6. 创作模型设计 + +## 6.1 拼图玩法采用 5 个高杠杆锚点 + +建议拼图玩法固定采用下面 `5` 个锚点,而不是沿用 RPG 的八锚点: + +1. `题材承诺` + - 这张拼图最想让玩家看到什么题材和幻想。 + +2. `画面主体` + - 这张图最核心的主体是什么,玩家切开之后仍然要认得出来的焦点是什么。 + +3. `视觉气质` + - 这张图更偏梦幻、悬疑、温暖、奇诡、机械、自然还是别的气质。 + +4. `拼图记忆点` + - 这张图切成网格后,哪些局部仍然能帮助玩家识别位置,例如轮廓、色块、构图层次、地标元素。 + +5. `标签与禁忌` + - 这张图应该挂哪些题材标签,不应该出现哪些元素。 + +## 6.2 锚点状态 + +和现有 Agent-first 创作流保持一致,每个锚点必须有明确状态: + +1. `待补充` +2. `推断中` +3. `已确认` +4. `已锁定` + +## 6.3 Agent 行为要求 + +拼图 Agent 必须做到: + +1. 优先接住创作者的画面灵感,而不是立刻列问卷。 +2. 每轮只追问当前最影响图片生成质量的 `1` 个问题。 +3. 当创作者已经说出足够信息时,优先总结,不重复追问。 +4. 在进入结果页前,至少确认: + - 一句题材承诺 + - 一个主要视觉主体 + - 一组气质描述 + - `3~6` 个题材标签 + - 至少 `1` 条禁止事项 + +## 6.4 建议数据结构 + +```ts +interface PuzzleCreatorIntent { + sourceMode: 'agent_chat'; + rawMessagesSummary: string; + themePromise: string; + visualSubject: string; + visualMood: string[]; + compositionHooks: string[]; + themeTags: string[]; + forbiddenDirectives: string[]; +} + +interface PuzzleAnchorPack { + intentSummary: string; + lockedAnchorIds: string[]; + canonicalTags: string[]; + imagePromptSummary: string; + focalSubjectSummary: string; + atmosphereSummary: string; + compositionSummary: string; +} +``` + +--- + +## 7. 结果页设计 + +## 7.1 结果页定位 + +拼图结果页是创作者从 Agent 共创转入正式发布前的最小工作台。 + +它至少承担 5 件事: + +1. 展示当前关卡名 +2. 管理拼图图片生成 +3. 展示并编辑题材标签 +4. 预览作者信息 +5. 执行发布 + +## 7.2 结果页必备字段 + +结果页中至少必须包含: + +1. `关卡名` +2. `AI 生成拼图图片` +3. `图片题材标签` + +本次建议同时加入: + +1. `Agent 理解摘要` +2. `创作者署名预览` +3. `图片生成状态` +4. `发布按钮` + +## 7.3 关卡名规则 + +关卡名生成规则建议如下: + +1. 默认由 Agent 根据锚点自动生成 `1` 个正式候选名。 +2. 创作者可直接手改。 +3. 关卡名长度建议控制在 `4~12` 个中文字符。 +4. 不允许空标题发布。 + +## 7.4 图片题材标签规则 + +标签必须满足: + +1. 每个作品正式标签数为 `3~6` 个。 +2. 标签必须是规范化后的短标签,不允许整句描述。 +3. 标签以中文短语为主,不默认改成英文。 +4. 发布前必须完成去重、去空格和别名归一。 + +建议的标签示例: + +1. `蒸汽城市` +2. `猫咪` +3. `雨夜` +4. `神庙遗迹` +5. `童话森林` + +## 7.5 AI 生成拼图图片 + +结果页中的图片生成区必须支持: + +1. 根据当前锚点生成正式拼图图片 +2. 重新生成 +3. 应用某一张候选图 + +第一版建议生成规则: + +1. 默认一次生成 `2` 张候选图 +2. 创作者选择 `1` 张作为正式图 +3. 正式图确定后,写回作品主图 + +## 7.6 拼图图片资产要求 + +拼图图片的正式资产要求: + +1. 官方拼图原图统一使用 `1:1` 正方形比例。 +2. 建议第一版正式生成尺寸为 `1536 x 1536`。 +3. 图中不允许生成标题字、水印、边框、按钮或 UI。 +4. 图像主体必须足够清晰,切成 `4*4` 后仍然有辨识信息。 + +## 7.7 结果页交互要求 + +结果页必须保持清爽,不默认塞玩法规则说明文案。 + +交互要求如下: + +1. 生成图片时打开独立面板,不在当前卡片下方内联堆出大块内容。 +2. 标签编辑应为轻量标签编辑器,不做大表单。 +3. 发布按钮必须固定清晰,不与图片生成操作混淆。 + +--- + +## 8. 拼图作品发布与广场分发 + +## 8.1 平台接入方式 + +平台现有创作中心和广场体系新增一个明确玩法类型: + +```ts +type PlatformWorkType = 'rpg' | 'puzzle'; +``` + +拼图玩法必须以独立玩法类型进入平台,而不是继续混在 RPG 世界作品下。 + +## 8.2 拼图广场卡片最小信息 + +拼图广场中的作品卡片至少展示: + +1. 作品主图 +2. 关卡名 +3. 作者名 +4. 题材标签 +5. 进入游戏按钮 + +## 8.3 玩家从广场进入的首关规则 + +当玩家从拼图广场点击某个作品进入时: + +1. 当前点击的作品必须成为当前 run 的第 `1` 关。 +2. 第 `1` 关不参与推荐计算,不做替换。 +3. 当前 run 的已游玩作品列表首先记入这个作品 id。 + +## 8.4 后续关卡推荐规则 + +第 `2` 关及以后,必须从“已发布且图片可用”的拼图作品池中选择下一关。 + +候选池要求: + +1. 必须是已发布作品。 +2. 必须有正式拼图图片。 +3. 必须有规范化标签。 +4. 默认优先排除当前 run 已经玩过的作品。 + +## 8.5 合并权重计算公式 + +后续关卡的推荐分数固定为: + +```ts +finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3; +``` + +其中: + +1. `tagSimilarityScore` + - 取值 `0 ~ 1` + - 表示候选作品标签与上一关标签的相似度 + +2. `sameAuthorScore` + - 同作者时固定为 `1` + - 非同作者时固定为 `0` + +## 8.6 标签相似度的建议算法 + +第一版建议使用规范化标签集合的加权 Jaccard 相似度: + +```ts +tagSimilarityScore = + intersectionWeight / unionWeight; +``` + +原因: + +1. 成本低 +2. 可解释性强 +3. 不依赖额外 embedding 基建 + +如果后续需要升级,再扩到“标签词向量 + 精确标签混合分”。 + +## 8.7 同分时的裁决顺序 + +若多个候选 `finalScore` 一致,则依次按下面顺序打破平分: + +1. `tagSimilarityScore` 更高者优先 +2. 当前 run 内未曝光过的作品优先 +3. 平台近期曝光更少的作品优先 +4. `updatedAt` 更新更近者优先 + +## 8.8 候选池兜底规则 + +如果排除已游玩作品后没有候选项,则: + +1. 允许回收当前 run 已游玩的作品重新进入池子 +2. 但不得连续两关重复同一个作品 + +--- + +## 9. 拼图运行时玩法系统 + +## 9.1 运行时一开始看到什么 + +玩家进入关卡后,必须直接看到全屏拼图画布。 + +运行时首屏组成: + +1. 全屏拼图舞台 +2. 顶部轻量 HUD +3. 最小化操作反馈 + +画面要求: + +1. 拼图舞台占满可用全屏区域 +2. 真正可操作的拼图棋盘按“最大正方形”填满安全区域 +3. 棋盘外延空间用同图模糊背景或纯净氛围底承接 +4. 不默认堆玩法说明文字 + +## 9.2 HUD 必显信息 + +游戏画面必须显示: + +1. 作者信息 +2. 关卡名 + +本次建议同时显示: + +1. 当前关卡序号 +2. 当前网格规格,例如 `3x3` 或 `4x4` + +## 9.3 难度与关卡推进规则 + +每次新 run 都必须从最低难度开始: + +1. 第 `1~3` 关固定为 `3x3` +2. 第 `4` 关开始固定为 `4x4` +3. 后续全部关卡保持 `4x4` + +对应函数建议: + +```ts +function resolvePuzzleGridSize(clearedLevelCount: number): 3 | 4 { + return clearedLevelCount >= 3 ? 4 : 3; +} +``` + +## 9.4 棋盘初始化规则 + +进入关卡时,后端必须: + +1. 根据正式图片切成 `x*x` 网格 +2. 为每块记录原始正确坐标 +3. 生成一个非完成态的打乱棋盘 + +本玩法允许任意两块交换,因此第一版不需要额外做奇偶性可解校验,只需要保证: + +1. 初始局面不是已完成态 +2. 初始局面至少存在可推进空间 + +## 9.5 交互规则总览 + +本玩法支持两类核心交互: + +1. `点击两块交换` +2. `拖动合并块` + +补充规则: + +1. 单块拖到合并块位置时,可以拆分合并块。 +2. 每次交换或拖放后,系统只重算受影响区域的连接关系。 + +## 9.6 点击两块交换 + +基础操作规则: + +1. 玩家点击第 `1` 块后,该块进入选中态。 +2. 玩家点击第 `2` 块后,系统执行交换。 +3. 若第 `2` 次点击仍是同一块,则取消选中。 + +第一版默认支持: + +1. 单块与单块交换 + +为了保证规则清晰,V1 不要求“两个不规则合并块直接点击互换”。 + +## 9.7 合并判定规则 + +每次交换完成后,系统必须判定“交换位置涉及到的拼块与其相邻块,是否在正确图像上也应当相邻”。 + +判定原则: + +1. 只看四向相邻: + - 上 + - 下 + - 左 + - 右 +2. 如果棋盘上两个块当前位置相邻,并且它们在原始图上的正确坐标也以相同方向相邻,则视为“正确连接”。 +3. 所有形成正确连接链的块,自动归并为同一个合并块。 + +## 9.8 合并反馈样式 + +当新的正确连接产生时,必须有轻微但明确的反馈: + +1. 轻微缩放吸附 +2. 边缘高亮闪一下 +3. 合并后边框弱化,整体感增强 + +反馈要克制,不能做成吵闹的强弹窗或大字提示。 + +## 9.9 合并块拖动规则 + +当多个块已经合并后,它们必须可以作为一个整体被拖动。 + +V1 规则如下: + +1. 合并块拖动时保持内部相对位置不变。 +2. 玩家拖动的是整个合并块,不是块内单独子块。 +3. 拖动松手后,系统按目标落点尝试执行整体重排。 + +## 9.10 单块拖入合并块时的拆分规则 + +这条规则是本玩法的关键差异点,必须明确落地: + +1. 当玩家拖动一个单块,落到某个已合并块占据的位置上时, +2. 系统必须先把目标合并块拆回独立单块, +3. 再根据实际落点完成本次单块交换或落位裁决, +4. 最后重新计算新的正确连接并重新合并。 + +也就是说: + +**合并不是永久锁死。单块对合并块发起操作时,目标合并块必须可被拆开。** + +## 9.11 重算范围 + +为了避免前端和后端做整盘重复计算,每次操作后只重算受影响区域: + +1. 本次发生交换的源格子 +2. 本次发生交换的目标格子 +3. 上述格子四向相邻的格子 +4. 被拆分或被拖动的合并块边界格子 + +## 9.12 通关判定 + +当下面任一条件成立时,判定当前关卡完成: + +1. 所有拼块合并成 `1` 个覆盖全盘的大合并块 +2. 所有拼块都回到原始正确位置 + +在正式实现中,建议以后端 `allTilesResolved = true` 作为唯一真相。 + +--- + +## 10. 运行时状态结构建议 + +## 10.1 作品结构 + +```ts +interface PuzzleProfile { + profileId: string; + ownerUserId: string; + authorDisplayName: string; + levelName: string; + summary: string; + themeTags: string[]; + coverImageSrc: string; + publicationStatus: 'draft' | 'published'; + anchorPack: PuzzleAnchorPack; +} +``` + +## 10.2 运行中关卡快照 + +```ts +interface PuzzleRuntimeLevelSnapshot { + runId: string; + levelIndex: number; + gridSize: 3 | 4; + profileId: string; + levelName: string; + authorDisplayName: string; + themeTags: string[]; + board: PuzzleBoardSnapshot; + status: 'playing' | 'cleared'; +} +``` + +## 10.3 棋盘快照 + +```ts +interface PuzzleBoardSnapshot { + rows: number; + cols: number; + pieces: PuzzlePieceState[]; + mergedGroups: PuzzleMergedGroupState[]; + selectedPieceId: string | null; +} + +interface PuzzlePieceState { + pieceId: string; + correctRow: number; + correctCol: number; + currentRow: number; + currentCol: number; + mergedGroupId: string | null; +} + +interface PuzzleMergedGroupState { + groupId: string; + pieceIds: string[]; + occupiedCells: Array<{ row: number; col: number }>; +} +``` + +## 10.4 当前 run 快照 + +```ts +interface PuzzleRunSnapshot { + runId: string; + entryProfileId: string; + clearedLevelCount: number; + currentLevelIndex: number; + currentGridSize: 3 | 4; + playedProfileIds: string[]; + previousLevelTags: string[]; + currentLevel: PuzzleRuntimeLevelSnapshot | null; +} +``` + +--- + +## 11. 前后端职责边界 + +## 11.1 前端职责 + +前端只负责: + +1. 展示 Agent 聊天界面 +2. 展示结果页 +3. 展示拼图画布、选中态、拖动反馈、合并反馈 +4. 发起交换、拖动、发布、开始游戏等请求 + +前端不负责: + +1. 解析锚点完成度 +2. 计算推荐下一关 +3. 计算标签相似度 +4. 判定哪些块应该合并 +5. 判定合并块何时拆分 +6. 判定通关 +7. 保存 run 状态 + +## 11.2 后端职责 + +`server-rs` 后端必须负责: + +1. 维护拼图 Agent 会话 +2. 编译拼图锚点和结果页草稿 +3. 生成和保存拼图图片资产 +4. 发布作品到拼图广场 +5. 创建 run +6. 初始化关卡棋盘 +7. 裁决交换、合并、拆分、拖动结果 +8. 判定通关 +9. 计算下一关推荐 +10. 保存当前 run 快照 + +--- + +## 11.3 前后端实现技术栈 + +根据当前仓库 `AGENTS.md` 的硬约束,这次拼图玩法的实现技术栈必须显式对齐为下面这套,而不能继续沿用 `server-node / Express / PostgreSQL` 方案: + +### 前端 + +1. 前端继续使用当前仓库既有的 `React + TypeScript + Vite` 壳层。 +2. 前端只负责: + - 页面表现 + - Agent 聊天输入 + - 结果页交互 + - 拼图画布渲染 + - HUD、选中态、拖动态、合并反馈表现 +3. 前端不得承担: + - 推荐算法 + - run 状态持久化 + - 拼块合并与拆分裁决 + - 通关判定 +4. 若后续拼图运行时需要实时订阅或读取 `SpacetimeDB` 数据,前端接入必须显式以 `spacetimedb-typescript` 约束为准。 + +### HTTP 与外部副作用层 + +1. 所有浏览器仍然只通过 `server-rs/crates/api-server` 暴露的 `Axum` HTTP 接口访问系统。 +2. `Axum` 负责: + - `/api/runtime/puzzle-*` 路由兼容面 + - 鉴权与请求上下文 + - SSE 或流式输出 + - 图片生成任务编排 + - 与 `SpacetimeDB` 的 procedure / reducer / 读模型聚合 + - 与 `OSS` 的上传确认、读链接派发 +3. `Axum` 是拼图玩法唯一对外 HTTP 边界,不新增浏览器直连三方服务的主链。 + +### 状态真相源 + +1. 拼图玩法的运行时状态、作品状态、Agent 会话状态、广场投影状态,统一以 `SpacetimeDB` 为唯一真相源。 +2. `SpacetimeDB` 中应承担: + - 拼图作品 profile 表 + - 拼图 Agent session / message / operation 表 + - 拼图 run 与关卡状态表 + - 拼块与合并组状态表 + - 拼图广场投影表 + - 标签相似度计算所需的规范化标签字段 +3. 所有真正修改状态的行为必须通过 reducer 或 procedure 完成,不能由 Axum 或前端偷偷改状态。 + +### 资产存储 + +1. 拼图正式图片资产统一存储到 `OSS`。 +2. `SpacetimeDB` 只保存资产对象引用、绑定关系、状态和元数据,不存二进制大对象。 +3. `Axum` 负责: + - 生成图片任务结束后的 OSS 上传 + - 资产对象确认 + - 对前端发放可读 URL 或兼容路径 + +### 目录与 crate 落点 + +当前仓库已经存在 `server-rs/` 多 crate 工作区,因此拼图玩法后端能力默认按下面结构落地: + +1. `server-rs/crates/api-server` + - 承载拼图 HTTP 路由、SSE、请求聚合 +2. `server-rs/crates/spacetime-module` + - 聚合拼图相关表、reducer、procedure、view +3. `server-rs/crates/shared-contracts` + - 放拼图玩法对前端公开的 DTO / envelope / SSE event +4. `server-rs/crates/spacetime-client` + - 供 `api-server` 调用 `SpacetimeDB` procedure / reducer / view +5. `server-rs/crates/platform-oss` + - 承载拼图图片对象上传与读链接能力 + +### 设计约束 + +1. 拼图玩法 PRD、实现和脚本只要涉及 `SpacetimeDB`,都必须显式按: + - `spacetimedb-cli` + - `spacetimedb-rust` + - `spacetimedb-concepts` + - `spacetimedb-typescript` + 执行。 +2. 若仓库中旧的 Node 文档或旧实现与本次拼图玩法的 Rust 后端方案冲突,必须先修正文档口径,再落工程。 + +--- + +## 12. 平台内命名规范 + +这次拼图玩法必须显式遵循平台内新的命名边界,避免继续沿用 `customWorld` 和 `rpgWorld` 前缀污染拼图域模型。 + +## 12.1 shared contract 文件命名 + +`packages/shared/src/contracts/` 下建议新增: + +1. `puzzleAgentSession.ts` +2. `puzzleAgentDraft.ts` +3. `puzzleAgentActions.ts` +4. `puzzleResultPreview.ts` +5. `puzzleWorkSummary.ts` +6. `puzzleRuntimeSession.ts` + +## 12.2 Axum 路由落点 + +`server-rs/crates/api-server/src/` 下建议新增拼图玩法对应模块文件: + +1. `puzzle_agent.rs` +2. `puzzle_works.rs` +3. `puzzle_gallery.rs` +4. `puzzle_runtime.rs` + +对应建议职责: + +1. `puzzle_agent.rs` + - 创建 session + - 获取 session + - 发送消息 + - 执行动作 +2. `puzzle_works.rs` + - owner-only works 列表 + - 单作品详情 + - upsert + - publish +3. `puzzle_gallery.rs` + - 公共广场列表 + - 公共作品详情 +4. `puzzle_runtime.rs` + - 创建 run + - 交换拼块 + - 拖动拼块/合并块 + - 推进下一关 + +## 12.3 Rust crate 与模块命名 + +### `shared-contracts` + +`server-rs/crates/shared-contracts/src/` 下建议新增: + +1. `puzzle_agent.rs` +2. `puzzle_runtime.rs` +3. `puzzle_works.rs` +4. `puzzle_gallery.rs` + +### `spacetime-module` + +拼图玩法的表、reducer、procedure、view 统一聚合到 `server-rs/crates/spacetime-module`,并由 `module-puzzle` 子 crate 承载领域实现。 + +建议新增 crate: + +1. `server-rs/crates/module-puzzle` + +其内部至少承载: + +1. 拼图作品表 +2. 拼图广场投影表 +3. 拼图 Agent session / message / operation 表 +4. 拼图 run / level / piece / merged-group 表 +5. 推荐计算和 run 推进相关 reducer / procedure + +### `api-server` + +若 `api-server` 需要拼图聚合逻辑,建议在 `server-rs/crates/api-server/src/` 内按文件拆分,不再引入 `server-node/src/services/` 式目录。 + +### `spacetime-client` + +`server-rs/crates/spacetime-client` 负责生成并封装拼图玩法的 DB 调用 facade,供 `api-server` 使用。 + +### `platform-oss` + +`server-rs/crates/platform-oss` 负责拼图图片对象的上传确认与读取地址派发。 + +命名原则: + +1. Agent 会话侧统一用 `puzzle_agent_*` +2. 已发布作品与广场聚合侧统一用 `puzzle_work_*` / `puzzle_gallery_*` +3. 运行时棋盘与推荐侧统一用 `puzzle_runtime_*` +4. Rust 文件与模块优先使用 `snake_case` + +## 12.4 前端组件命名 + +`src/components/` 下建议新增: + +1. `puzzle-home/PuzzleCreationHub.tsx` +2. `puzzle-agent/PuzzleAgentWorkspace.tsx` +3. `puzzle-agent/PuzzleAgentThread.tsx` +4. `puzzle-result/PuzzleResultView.tsx` +5. `puzzle-gallery/PuzzleGalleryView.tsx` +6. `puzzle-runtime-shell/PuzzleRuntimeShell.tsx` +7. `puzzle-runtime-panels/PuzzleHud.tsx` +8. `puzzle-runtime-panels/PuzzleCanvas.tsx` + +## 12.5 平台脚本命名规范 + +仓库根目录 `scripts/` 下,如果新增拼图专项脚本,必须遵守现有脚本命名风格: + +1. 文件名使用 kebab-case +2. 校验脚本用 `validate-*` +3. 冒烟脚本用 `smoke-*` +4. 不直接把玩法逻辑塞进 `custom-world-*` 或 `rpg-*` 旧脚本 + +建议命名如下: + +1. `validate-puzzle-content.ts` +2. `smoke-puzzle-agent.ts` +3. `smoke-puzzle-runtime.ts` +4. `smoke-puzzle-gallery.ts` + +## 12.6 明确禁止 + +本次明确禁止下面这些命名: + +1. `customWorldPuzzle...` +2. `rpgPuzzle...` +3. `customWorldAgent` 下继续承载拼图会话逻辑 +4. `RpgWorld...` 下继续承载拼图广场或拼图作品聚合逻辑 + +--- + +## 13. API 设计建议 + +## 13.1 Agent 创作链 + +统一建议前缀: + +`/api/runtime/puzzle-agent` + +建议接口: + +1. `POST /api/runtime/puzzle-agent/sessions` +2. `GET /api/runtime/puzzle-agent/sessions/:sessionId` +3. `POST /api/runtime/puzzle-agent/sessions/:sessionId/messages` +4. `POST /api/runtime/puzzle-agent/sessions/:sessionId/actions` + +## 13.2 作品与广场链 + +统一建议前缀: + +1. `/api/runtime/puzzle-works` +2. `/api/runtime/puzzle-gallery` + +建议接口: + +1. `GET /api/runtime/puzzle-works` +2. `GET /api/runtime/puzzle-works/:profileId` +3. `PUT /api/runtime/puzzle-works/:profileId` +4. `POST /api/runtime/puzzle-works/:profileId/publish` +5. `GET /api/runtime/puzzle-gallery` +6. `GET /api/runtime/puzzle-gallery/:ownerUserId/:profileId` + +## 13.3 运行时链 + +统一建议前缀: + +`/api/runtime/puzzle-runtime` + +建议接口: + +1. `POST /api/runtime/puzzle-runtime/runs` +2. `GET /api/runtime/puzzle-runtime/runs/:runId` +3. `POST /api/runtime/puzzle-runtime/runs/:runId/swap` +4. `POST /api/runtime/puzzle-runtime/runs/:runId/drag` +5. `POST /api/runtime/puzzle-runtime/runs/:runId/next-level` + +--- + +## 14. 前端界面要求 + +## 14.1 创作中心入口 + +平台创作中心新增一个拼图玩法入口卡片。 + +卡片只需要表达: + +1. 这是拼图玩法 +2. 可以通过 Agent 共创图片关卡 + +不需要在卡片里写一大段玩法规则说明。 + +## 14.2 Agent 工作区 + +拼图 Agent 工作区应比 RPG 更轻: + +1. 主聊天流 +2. 顶部当前锚点摘要 +3. 进入结果页按钮 + +不需要默认展示角色、地点、势力这类重卡片抽屉。 + +## 14.3 结果页 + +结果页必须移动端优先,并且信息密度克制。 + +建议结构: + +1. 顶部关卡名 +2. 中部图片预览 +3. 图片生成按钮 +4. 标签区 +5. 发布按钮 + +## 14.4 拼图运行时界面 + +运行时必须以全屏拼图舞台为绝对主角。 + +建议布局: + +1. 顶部轻量 HUD +2. 中间最大正方形拼图棋盘 +3. 底部不常驻大段文案 + +如需操作提示,只允许短暂轻提示,不允许占据长期版面。 + +--- + +## 15. 校验与测试要求 + +## 15.1 发布阻断项 + +拼图作品发布前至少要检查: + +1. 关卡名非空 +2. 正式拼图图片已确定 +3. 正式标签数量满足 `3~6` +4. 作者信息可读 + +若上述任一缺失,必须阻止发布。 + +## 15.2 运行时核心测试 + +必须补下面这些测试: + +1. `puzzleTagSimilarityService.test.ts` +2. `puzzleLevelSelectionService.test.ts` +3. `puzzleMergeResolutionService.test.ts` +4. `PuzzleRuntimeShell.interaction.test.tsx` + +## 15.3 冒烟脚本 + +建议至少补下面这些脚本: + +1. `scripts/smoke-puzzle-agent.ts` +2. `scripts/smoke-puzzle-runtime.ts` +3. `scripts/smoke-puzzle-gallery.ts` + +## 15.4 编码检查 + +涉及中文文档和中文标签的改动后,必须继续通过: + +1. `npm run check:encoding` + +--- + +## 16. 推荐落地顺序 + +## 阶段 A:先接平台入口与 Agent 共创 + +先做: + +1. 拼图玩法入口 +2. `puzzle-agent` 会话 +3. `5` 锚点提取和结果页生成 + +完成标准: + +1. 创作者能从平台进入拼图 Agent 工作区 +2. 能通过聊天生成结果页草稿 + +## 阶段 B:再做结果页与图片资产 + +先做: + +1. 关卡名 +2. 标签编辑 +3. AI 图片生成 +4. 发布链 + +完成标准: + +1. 创作者能生成正式拼图图片并发布 +2. 作品能进入拼图广场 + +## 阶段 C:再做拼图运行时核心循环 + +先做: + +1. `3x3 / 4x4` 切图 +2. 点击两块交换 +3. 正确连接自动合并 +4. 合并块整体拖动 +5. 单块拖入合并块触发拆分 + +完成标准: + +1. 玩家能完整打通单关 + +## 阶段 D:最后做广场接续关卡链 + +先做: + +1. 从广场进入第 `1` 关 +2. 通关后推荐第 `2` 关 +3. “标签 `70%` + 同作者 `30%`”正式生效 + +完成标准: + +1. 玩家能连续游玩多关 +2. 关卡推荐可解释、可复现 + +--- + +## 17. 验收标准 + +当下面这些结果都成立时,视为这次拼图玩法 PRD 被正确落地: + +1. 平台内已经有独立的拼图玩法创作入口。 +2. 拼图创作流程先经过 Agent 聊天确定高杠杆锚点,再进入结果页。 +3. 结果页至少具备关卡名、AI 生成拼图图片、图片题材标签。 +4. 发布后的拼图作品能进入平台广场。 +5. 玩家从广场进入时,第 `1` 关必定是当前作品本身。 +6. 第 `2` 关及以后按照“标签相似度 `70%` + 同作者 `30%`”计算下一关。 +7. 新 run 前 `3` 关为 `3x3`,之后固定为 `4x4`。 +8. 运行时支持点击两块交换。 +9. 交换后正确相邻的块会自动合并。 +10. 合并块可以整体拖动。 +11. 单块拖到合并块位置时可以拆分合并块。 +12. 游戏画面能显示作者信息和关卡名。 +13. 拼图玩法没有继续错误复用 `customWorld` 或 `rpgWorld` 域命名。 +14. 新增脚本命名符合平台现有规范。 + +--- + +## 18. 一句话结论 + +这次平台新增拼图玩法,正确的做法不是只补一个拼图画布,而是: + +**把拼图作为平台内独立玩法类型接进现有 Agent-first 创作中心、结果页、发布链、广场分发链和运行时链,让创作者先收束高杠杆视觉锚点,让玩家在全屏交换-合并-拆分的拼图循环中持续游玩。** diff --git a/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md b/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md new file mode 100644 index 00000000..51f95f8f --- /dev/null +++ b/docs/technical/BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md @@ -0,0 +1,273 @@ +# 大鱼吃小鱼玩法创作与运行态最小落地技术方案 + +日期:`2026-04-22` + +## 1. 文档目的 + +本文件承接 PRD《AI 原生 Agent-First 大鱼吃小鱼玩法创作工具与玩法系统》,冻结本轮工程落地的最小完整闭环。 + +本轮目标不是抽象一个通用街机玩法引擎,而是在现有平台内新增一个独立 `big_fish` 玩法域,跑通: + +1. 平台创作入口选择大鱼吃小鱼玩法 +2. Agent 会话创建、消息提交和 SSE 兼容返回 +3. 基于 4 个高杠杆锚点编译玩法草稿 +4. 结果页生成等级主图、等级动作、场地背景的正式资产槽位 +5. 发布校验 +6. 启动测试运行态 +7. 后端推进摇杆输入、刷怪、吞噬收编、三合一、屏外清理和胜负裁决 + +## 2. 本轮明确不做 + +1. 不接入真实图片 / 动作模型调用,只生成可预览的占位资产引用与冻结提示词快照。 +2. 不新增 WebSocket 依赖;首版运行态使用 `POST input + GET snapshot` 的有限 HTTP 辅助接口,后续再升级长连接。 +3. 不把 Big Fish 写回 `custom_world`、`rpgCreation` 或 RPG runtime 旧语义。 +4. 不新增作品市场、排行榜、复盘、局外成长、PvP。 +5. 不要求前端本地模拟真相;前端只渲染后端 snapshot。 + +## 3. Rust crate 边界 + +新增: + +1. `server-rs/crates/module-big-fish` + - 纯领域模型、输入校验、草稿编译、资产覆盖率、运行态规则推进。 + - 可开启 `spacetime-types` feature,为 `spacetime-module` 派生 SpacetimeDB 类型。 + +接入: + +1. `server-rs/crates/spacetime-module` + - 新增 Big Fish 表与 procedure。 + - 只存状态与结构化引用,不做 OSS / LLM 外部 IO。 +2. `server-rs/crates/spacetime-client` + - 新增 Big Fish facade,隐藏 generated bindings。 +3. `server-rs/crates/shared-contracts` + - 新增 HTTP DTO。 +4. `server-rs/crates/api-server` + - 新增 `big_fish_creation.rs`、`big_fish_assets.rs`、`big_fish_runtime.rs` 或最小合并的 `big_fish.rs`。 + +## 4. SpacetimeDB 表 + +本轮只新增必要表,所有表主键使用 Axum 生成的显式业务 ID。 + +### 4.1 `big_fish_creation_session` + +字段: + +1. `session_id: String` +2. `owner_user_id: String` +3. `seed_text: String` +4. `current_turn: u32` +5. `progress_percent: u32` +6. `stage: BigFishCreationStage` +7. `anchor_pack_json: String` +8. `draft_json: Option` +9. `asset_coverage_json: String` +10. `last_assistant_reply: Option` +11. `publish_ready: bool` +12. `created_at: Timestamp` +13. `updated_at: Timestamp` + +索引: + +1. `by_big_fish_session_owner_user_id(owner_user_id)` + +### 4.2 `big_fish_agent_message` + +字段: + +1. `message_id: String` +2. `session_id: String` +3. `role: BigFishAgentMessageRole` +4. `kind: BigFishAgentMessageKind` +5. `text: String` +6. `created_at: Timestamp` + +索引: + +1. `by_big_fish_message_session_id(session_id)` + +### 4.3 `big_fish_asset_slot` + +字段: + +1. `slot_id: String` +2. `session_id: String` +3. `asset_kind: BigFishAssetKind` +4. `level: Option` +5. `motion_key: Option` +6. `status: BigFishAssetStatus` +7. `asset_url: Option` +8. `prompt_snapshot: String` +9. `updated_at: Timestamp` + +索引: + +1. `by_big_fish_asset_session_id(session_id)` +2. `by_big_fish_asset_slot(session_id, asset_kind, level, motion_key)` + +### 4.4 `big_fish_runtime_run` + +字段: + +1. `run_id: String` +2. `session_id: String` +3. `owner_user_id: String` +4. `status: BigFishRunStatus` +5. `snapshot_json: String` +6. `last_input_x: f32` +7. `last_input_y: f32` +8. `tick: u64` +9. `created_at: Timestamp` +10. `updated_at: Timestamp` + +索引: + +1. `by_big_fish_run_owner_user_id(owner_user_id)` +2. `by_big_fish_run_session_id(session_id)` + +## 5. SpacetimeDB procedure + +本轮全部使用 procedure 同步返回快照,避免 Axum 额外拼读模型。 + +1. `create_big_fish_session(input) -> BigFishSessionProcedureResult` +2. `get_big_fish_session(input) -> BigFishSessionProcedureResult` +3. `submit_big_fish_message(input) -> BigFishSessionProcedureResult` +4. `compile_big_fish_draft(input) -> BigFishSessionProcedureResult` +5. `generate_big_fish_asset(input) -> BigFishSessionProcedureResult` +6. `publish_big_fish_game(input) -> BigFishSessionProcedureResult` +7. `start_big_fish_run(input) -> BigFishRunProcedureResult` +8. `submit_big_fish_input(input) -> BigFishRunProcedureResult` +9. `get_big_fish_run(input) -> BigFishRunProcedureResult` + +说明: + +1. `submit_big_fish_message` 只做 deterministic 锚点补全,不调用 LLM。 +2. `generate_big_fish_asset` 只写正式资产槽位和提示词快照,真实模型生成后续由 AI/OSS worker 替换。 +3. `submit_big_fish_input` 每次至少推进 1 个后端 tick,前端不能本地裁决。 +4. 运行态所有“持续时间”语义按真实秒数累计,前端即使摇杆静止也要持续以当前输入心跳驱动后端推进,避免刷怪与屏外 `3` 秒清理依赖手速或提交频率。 + +## 6. HTTP contract + +所有接口挂在 `/api/runtime/big-fish/*`,全部需要 Bearer 鉴权。 + +开发态本地链路补充约定: + +1. 浏览器仍只请求同源 `/api/runtime/big-fish/*`。 +2. `vite -> server-node:8081` 保持不变,由 `server-node` 先复用现有登录态完成 Bearer 校验。 +3. `server-node` 仅作为 Big Fish 兼容网关,把“已校验用户身份 + 原始请求体”转发到 Rust `api-server`。 +4. Rust `api-server` 继续作为 Big Fish 真相后端,正式处理会话、草稿、资产动作和运行态规则。 +5. 本地默认端口: + - `vite`: `3000` + - `server-node`: `8081` + - Rust `api-server`: `3100` + - `SpacetimeDB standalone`: `3001` +6. `GENARRATIVE_SPACETIME_DATABASE` 本地开发优先跟随仓库根目录 `spacetime.local.json` 的 `database` 字段,避免 `api-server` 默认连到错误数据库名。 + - `.env.local` 或进程环境显式配置 `GENARRATIVE_SPACETIME_DATABASE` 时可覆盖本地配置。 + - `.env.example` 只提供示例默认值,不得压过 `spacetime.local.json`。 + +### 6.1 创作会话 + +1. `POST /api/runtime/big-fish/agent/sessions` +2. `GET /api/runtime/big-fish/agent/sessions/{sessionId}` +3. `POST /api/runtime/big-fish/agent/sessions/{sessionId}/messages/stream` +4. `POST /api/runtime/big-fish/agent/sessions/{sessionId}/actions` + +`messages/stream` 首版兼容当前前端 SSE 解析方式,只输出: + +1. `reply_delta` +2. `session` +3. `done` +4. `error` + +`actions` 首版支持: + +1. `big_fish_compile_draft` +2. `big_fish_generate_level_main_image` +3. `big_fish_generate_level_motion` +4. `big_fish_generate_stage_background` +5. `big_fish_publish_game` + +### 6.2 运行态 + +1. `POST /api/runtime/big-fish/sessions/{sessionId}/runs` +2. `GET /api/runtime/big-fish/runs/{runId}` +3. `POST /api/runtime/big-fish/runs/{runId}/input` + +`input` 请求体: + +```json +{ + "x": 0.4, + "y": -0.2 +} +``` + +## 7. 运行态最小规则 + +后端推进规则固定: + +1. 开局拥有 1 个 `level = 1` 己方实体。 +2. 开局视野内生成至少 2 个同级野生实体。 +3. 己方实体碰撞低于或等于自己等级的野生实体时收编。 +4. 高于己方等级的野生实体碰撞己方实体时吃掉该己方实体。 +5. 每次结算后从低等级开始做三合一连锁合成。 +6. 野生实体池围绕玩家最高己方等级维持低 1~2 级与高 1~2 级。 +7. 同等级、高 3 级及以上、低 3 级及以下的野生实体,屏外连续 3 秒后删除。 +8. 玩家首次拥有最高等级实体时立即胜利。 +9. 己方实体归零时失败。 + +## 8. 前端接入边界 + +新增目录: + +1. `src/services/big-fish-creation/` +2. `src/components/big-fish-creation/` +3. `src/components/big-fish-result/` +4. `src/components/big-fish-runtime/` + +复用现有平台入口壳层,但入口脚本必须使用通用平台命名,禁止把 Big Fish 业务状态写进 `rpg-entry` 命名脚本: + +1. 在 `src/components/platform-entry/PlatformEntryCreationTypeModal.tsx` 新增“大鱼吃小鱼”选项。 +2. 在 `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx` 中新增 Big Fish 专属 stage。 +3. Big Fish 不使用 `RpgCreationResultView`,使用 `BigFishResultView`。 +4. `src/components/rpg-entry/*` 只能保留兼容导出或 RPG 专属组件,不允许承载 Big Fish 业务分支。 + +前端只允许: + +1. 展示会话、草稿、资产槽位、运行快照。 +2. 发送聊天、action 和摇杆输入。 +3. 根据后端 snapshot 渲染实体。 + +前端禁止: + +1. 自行决定刷怪。 +2. 自行决定吞噬 / 合成 / 清理 / 胜负。 + +## 9. 本轮验收 + +完成后至少执行: + +1. `cargo fmt -p module-big-fish -p shared-contracts -p spacetime-module -p spacetime-client -p api-server` +2. `cargo check -p module-big-fish` +3. `cargo check -p shared-contracts` +4. `cargo check -p spacetime-module` +5. `spacetime generate` 刷新 Rust bindings +6. `cargo check -p spacetime-client` +7. `cargo check -p api-server` +8. 前端类型 / 构建检查 +9. `npm run check:encoding` + +## 10. 本地开发补充 + +为避免再次出现 `POST /api/runtime/big-fish/agent/sessions` 命中旧 Node 后端 `404`,本轮额外冻结以下联调口径: + +1. `npm run dev` 需要同时拉起: + - `server-node` + - Rust `api-server` + - `vite` +2. `server-node` 新增 Big Fish 专用兼容网关路由,不在 Node 内复制 Big Fish 玩法逻辑。 +3. Node -> Rust 转发使用内部桥接头: + - `x-genarrative-authenticated-user-id` + - `x-genarrative-internal-api-secret` +4. Rust 侧只对带正确内部密钥的本地桥接请求接受该用户头,不对普通外部请求放开匿名身份伪造。 + +如检查发现本轮主链缺口,继续补齐;如已经满足上述验收,不继续扩展额外玩法能力。 diff --git a/docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md b/docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md new file mode 100644 index 00000000..66d92ed6 --- /dev/null +++ b/docs/technical/PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md @@ -0,0 +1,384 @@ +# 拼图玩法创作与运行态最小落地技术方案 + +日期:`2026-04-22` + +## 1. 文档目的 + +本文件承接 PRD《AI 原生拼图玩法创作工具与玩法系统》,冻结本轮拼图玩法在当前平台内的最小完整闭环。 + +本轮目标不是抽象一个通用拼图编辑器,也不是额外搭建一个脱离平台的新小游戏站点,而是在现有平台壳层内新增独立 `puzzle` 玩法域,跑通下面这条主链: + +1. 平台创作入口选择拼图玩法 +2. Agent-first 对话收束 5 个高杠杆视觉锚点 +3. 编译结果页草稿 +4. 在结果页编辑关卡名、标签并生成候选拼图图片 +5. 发布作品进入拼图广场 +6. 玩家从广场进入第 1 关 +7. 后端初始化 `3x3 / 4x4` 棋盘 +8. 后端裁决交换、合并、拖动、拆分与通关 +9. 通关后根据“标签相似度 `70%` + 同作者 `30%`”推荐下一关 + +## 2. 本轮明确不做 + +1. 不做异形拼块、旋转拼块、道具、体力、倒计时。 +2. 不做新的平台站点或新的全局导航系统。 +3. 不做前端本地推荐、前端本地裁决、前端本地持久化真相。 +4. 不做复杂图片模型编排;首版图片生成沿用 `api-server` 的占位资产生成方式,保证完整链路可跑通。 +5. 不把拼图玩法继续挂在 `customWorld`、`rpgWorld` 或 RPG runtime 旧语义下。 +6. 不扩到拼图排行榜、社交评论、收藏、复盘系统。 + +## 3. 分层边界 + +### 3.1 前端 + +前端继续使用当前 `React + TypeScript + Vite` 平台壳层,只负责: + +1. 展示拼图创作中心、Agent 工作区、结果页、广场、运行时画布。 +2. 发起聊天、结果页编辑、发布、开始游戏、交换与拖动请求。 +3. 基于后端快照渲染棋盘、HUD、选中态与合并反馈。 + +前端禁止: + +1. 自行判断下一关推荐。 +2. 自行判断拼块是否应当合并。 +3. 自行判断合并块是否应当拆分。 +4. 自行判断通关。 + +### 3.2 Axum + +`server-rs/crates/api-server` 负责: + +1. 对外暴露 `/api/runtime/puzzle-*` HTTP 接口。 +2. 鉴权、请求上下文、错误 envelope。 +3. 结果页占位图片生成与静态资产落盘。 +4. 调用 `spacetime-client` 读写拼图玩法真相态。 + +### 3.3 SpacetimeDB + +`server-rs/crates/spacetime-module` 负责: + +1. 存储拼图 Agent session / message。 +2. 存储已发布拼图作品 profile。 +3. 存储拼图运行态 run snapshot。 +4. 通过 procedure 同步返回 session / works / gallery / runtime 快照。 + +### 3.4 纯领域 crate + +新增 `server-rs/crates/module-puzzle`,承载: + +1. 5 个锚点与会话阶段的纯领域模型。 +2. 草稿编译、标签规范化、发布校验。 +3. `3x3 / 4x4` 棋盘初始化。 +4. 交换、合并、拖动、拆分、通关与下一关推荐算法。 + +## 4. 共享契约 + +### 4.1 TypeScript shared contracts + +在 `packages/shared/src/contracts/` 新增: + +1. `puzzleAgentSession.ts` +2. `puzzleAgentDraft.ts` +3. `puzzleAgentActions.ts` +4. `puzzleResultPreview.ts` +5. `puzzleWorkSummary.ts` +6. `puzzleRuntimeSession.ts` + +这些文件分别承载: + +1. Agent session / message / anchor pack +2. 结果页草稿与候选图片 +3. Agent actions 与 works/gallery mutation request +4. 结果页 publish gate / preview +5. owner-only works 与 gallery card +6. runtime run / board / swap / drag / next-level contract + +### 4.2 Rust shared contracts + +在 `server-rs/crates/shared-contracts/src/` 新增: + +1. `puzzle_agent.rs` +2. `puzzle_works.rs` +3. `puzzle_gallery.rs` +4. `puzzle_runtime.rs` + +Rust DTO 只承载对前端公开的 HTTP contract,不直接泄露 `module-puzzle` 内部实现细节。 + +## 5. Spacetime 表与 procedure + +本轮保持“最小闭环优先”,作品与运行时仍以结构化字段 + `snapshot_json` 组合持久化,不额外拆出更多高耦合表。 + +### 5.1 `puzzle_agent_session` + +字段: + +1. `session_id` +2. `owner_user_id` +3. `seed_text` +4. `current_turn` +5. `progress_percent` +6. `stage` +7. `anchor_pack_json` +8. `draft_json` +9. `last_assistant_reply` +10. `published_profile_id` +11. `created_at` +12. `updated_at` + +### 5.2 `puzzle_agent_message` + +字段: + +1. `message_id` +2. `session_id` +3. `role` +4. `kind` +5. `text` +6. `created_at` + +### 5.3 `puzzle_work_profile` + +字段: + +1. `profile_id` +2. `owner_user_id` +3. `source_session_id` +4. `author_display_name` +5. `level_name` +6. `summary_text` +7. `theme_tags_json` +8. `cover_image_src` +9. `cover_asset_id` +10. `anchor_pack_json` +11. `publication_status` +12. `play_count` +13. `updated_at` +14. `published_at` + +### 5.4 `puzzle_runtime_run` + +字段: + +1. `run_id` +2. `owner_user_id` +3. `entry_profile_id` +4. `current_profile_id` +5. `cleared_level_count` +6. `current_level_index` +7. `current_grid_size` +8. `played_profile_ids_json` +9. `previous_level_tags_json` +10. `snapshot_json` +11. `updated_at` +12. `created_at` + +### 5.5 Procedure + +本轮全部使用 procedure 同步返回快照,避免 Axum 再次读 private table: + +1. `create_puzzle_agent_session` +2. `get_puzzle_agent_session` +3. `submit_puzzle_agent_message` +4. `compile_puzzle_agent_draft` +5. `save_puzzle_generated_images` +6. `select_puzzle_cover_image` +7. `publish_puzzle_work` +8. `list_puzzle_works` +9. `get_puzzle_work_detail` +10. `update_puzzle_work` +11. `list_puzzle_gallery` +12. `get_puzzle_gallery_detail` +13. `start_puzzle_run` +14. `get_puzzle_run` +15. `swap_puzzle_pieces` +16. `drag_puzzle_piece_or_group` +17. `advance_puzzle_next_level` + +## 6. 结果页图片生成策略 + +本轮不引入新的真实图像模型编排,而是复用 `api-server` 里已有的占位资产写盘模式: + +1. 每次生成 2 张候选图。 +2. 候选图通过 `api-server` 写入 `public/generated-puzzle-covers/...`。 +3. Axum 把候选图 URL、assetId、prompt snapshot 回写到 Spacetime session draft。 +4. 创作者在结果页选择其中 1 张作为正式图。 + +这样可以保证: + +1. 结果页图片生成、重生、应用正式图完整可用。 +2. 发布链有正式图片可校验。 +3. 不额外扩到模型供应商集成。 + +### 6.1 发布前编辑真相补充 + +结果页允许创作者在发布前直接编辑: + +1. `关卡名` +2. `摘要` +3. `题材标签` + +这 3 个字段不能只停留在前端临时态。 + +本轮冻结为: + +1. `publish_puzzle_work` 允许直接携带 `levelName / summary / themeTags` +2. `spacetime-module` 在发布事务内先把这些字段覆盖回 session draft 真相 +3. 覆盖后的 draft 再参与发布校验与 profile 持久化 + +这样可以避免额外新增一条“草稿轻量编辑 procedure”,同时确保结果页编辑内容会真实进入广场作品与后续运行时 HUD。 + +## 7. 运行态规则冻结 + +### 7.1 难度推进 + +```ts +function resolvePuzzleGridSize(clearedLevelCount: number): 3 | 4 { + return clearedLevelCount >= 3 ? 4 : 3; +} +``` + +### 7.2 棋盘初始化 + +1. 根据正式图片与网格规格生成 `pieceId -> correctRow/correctCol`。 +2. 随机打乱到非完成态。 +3. 生成初始 `mergedGroups = []`,再执行一次正确连接检查。 + +### 7.3 正确连接 + +若两个拼块在当前棋盘中四向相邻,且它们在原图上的正确位置也以同方向相邻,则视为正确连接。 + +所有正确连接链通过并查集合并为 `mergedGroup`。 + +### 7.4 拖动与拆分 + +1. 单块拖到单块位置:执行交换。 +2. 合并块拖到任意目标锚点:保持内部相对布局整体重排。 +3. 单块拖到合并块占据位置:先拆分目标合并块,再执行交换,最后重算合并。 + +### 7.5 通关 + +当所有拼块回到正确位置,或全盘只剩一个覆盖全部拼块的合并组时,标记当前关卡 `cleared`。 + +### 7.6 下一关推荐 + +固定公式: + +```ts +finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3; +``` + +标签相似度首版使用规范化标签集合的 Jaccard。 + +同分裁决顺序: + +1. `tagSimilarityScore` 更高 +2. 当前 run 未出现过 +3. `play_count` 更低 +4. `updated_at` 更近 + +## 8. 前端接入 + +### 8.1 平台入口 + +只改现有平台壳层: + +1. 在创作类型弹层新增“拼图玩法”。 +2. 新增拼图专属 stage,不改 RPG runtime 主链。 + +### 8.2 组件目录 + +新增: + +1. `src/components/puzzle-agent/` +2. `src/components/puzzle-result/` +3. `src/components/puzzle-gallery/` +4. `src/components/puzzle-runtime/` + +### 8.3 服务目录 + +新增: + +1. `src/services/puzzle-agent/` +2. `src/services/puzzle-works/` +3. `src/services/puzzle-gallery/` +4. `src/services/puzzle-runtime/` + +本轮全部走 HTTP facade,不引入新的前端 Spacetime 直连。 + +### 8.4 当前前端最小落地补充 + +当前实现固定走下面这条最小链路: + +1. `PlatformEntryCreationTypeModal` 选择 `puzzle` +2. `PuzzleAgentWorkspace` 收束锚点并触发 `compile_puzzle_draft` +3. `PuzzleResultView` 编辑 `levelName / summary / themeTags` +4. 图片生成通过独立 `PuzzleImageStudioModal` 触发,不在结果页内联堆叠 +5. 发布后跳转 `PuzzleGalleryDetailView` +6. 从详情进入 `PuzzleRuntimeShell` + +创作中心作品展示冻结为: + +1. 拼图作品也是平台作品,和其他创作作品共用同一套列表项样式。 +2. 创作中心不再保留独立“拼图玩法作品模块”。 +3. 拼图作品仅通过 `拼图` 标签与题材标签区分,不额外拆出第二块作品区。 +4. 创作中心仍保留统一“新建作品”入口,由创建类型弹层继续分流到 RPG / 拼图玩法。 + +运行时前端表现冻结为: + +1. 使用正式封面图按 `correctRow / correctCol` 做真实网格切片渲染 +2. 点击两块时仅前端维护轻量选中态,真正交换以后端返回快照为准 +3. 拖动统一采用 pointer 事件,兼顾网页端与移动端 +4. 不在前端计算合并、拆分、通关与下一关推荐 + +## 9. 验收与检查 + +完成后至少执行: + +1. `npm run check:encoding` +2. `npm run typecheck` +3. `npm run test` +4. `cargo check -p module-puzzle` +5. `cargo check -p shared-contracts` +6. `cargo check -p spacetime-module` +7. `spacetime generate --no-config --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module --include-private --yes` +8. `cargo check -p spacetime-client` +9. `cargo check -p api-server` + +如果检查中发现拼图主链缺口,继续补齐;如果已经满足 PRD 主链和上述检查,不再追加额外玩法能力。 + +## 10. 2026-04-22 最终验收记录 + +本轮已按“最小完整闭环、禁止超出需求过度实现”完成拼图玩法主链落地,并补齐收尾检查。 + +### 10.1 已落地主链 + +1. 平台创作中心可选择 `puzzle` 玩法入口。 +2. `PuzzleAgentWorkspace` 已接入 Agent-first 锚点收束与草稿编译。 +3. `PuzzleResultView` 已支持最小结果页编辑与独立图片生成弹层。 +4. 发布后作品可进入拼图广场与详情页。 +5. `PuzzleRuntimeShell` 已按正式封面图真实切片渲染 `3x3 / 4x4` 关卡。 +6. 交换、拖动、拆分、合并、通关、下一关推荐真相全部以后端快照为准。 + +### 10.2 本轮额外修复的验收阻塞 + +在最终验收阶段,补齐了与拼图主链无直接业务耦合、但会阻塞仓库整体检查的基线问题: + +1. `typecheck` 基线类型不兼容。 +2. `AccountModal` 测试 mock 字段落后于最新鉴权契约。 +3. `customWorld` 存档归一化中场景连接方向未收敛到强类型。 +4. 结果页生成资源在签名 URL 尚未返回时会短暂空白,已调整为先展示原路径占位,再异步替换签名读地址。 + +### 10.3 实际通过的检查 + +1. `npm run check:encoding` +2. `npm run typecheck` +3. `npm run test` +4. `cargo check -p module-puzzle` +5. `cargo check -p shared-contracts` +6. `cargo check -p spacetime-module` +7. `cargo check -p spacetime-client` +8. `cargo check -p api-server` + +### 10.4 冻结说明 + +截至本次验收,拼图玩法已满足 PRD 要求的最小产品闭环;未继续扩展排行榜、提示、体力、异形拼块、倒计时、前端本地裁决等超出本轮需求的能力。 diff --git a/docs/technical/README.md b/docs/technical/README.md index 536366fd..755cf75e 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -42,6 +42,9 @@ - [SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_SESSION_STAGE6_DESIGN_2026-04-22.md):冻结 `M5` Agent session create / snapshot 的最小 SpacetimeDB 与 Axum facade 闭环,明确本轮不迁移 LLM、SSE、卡片更新和完整 action registry。 - [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STAGE7_DESIGN_2026-04-22.md):冻结 `M5` Agent `message submit / operation query` 的 deterministic 最小闭环,明确同步写入 user/assistant 消息、`process_message` operation 与 session 进度推进规则。 - [SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_AGENT_MESSAGE_STREAM_STAGE8_DESIGN_2026-04-22.md):冻结 `M5` Agent `/messages/stream` 的最小兼容 SSE facade,明确复用 Stage 7 的同步写表逻辑,只输出当前前端真实消费的 `reply_delta / session / done / error` 事件。 +- [BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./BIG_FISH_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结大鱼吃小鱼玩法本轮最小完整落地方案,明确 `module-big-fish`、SpacetimeDB 表 / procedure、Axum facade、前端接入和运行态规则边界。 +- [PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md](./PUZZLE_CREATION_AND_RUNTIME_MINIMAL_IMPLEMENTATION_2026-04-22.md):冻结拼图玩法本轮最小完整落地方案,明确 `module-puzzle`、SpacetimeDB 表 / procedure、Axum facade、前端接入,以及交换 / 合并 / 拖动 / 拆分 / 下一关推荐边界。 +- [UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md](./UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md):冻结所有创作品类 Agent 聊天 UI 与对话进度管理统一框架,明确品类差异只保留锚点映射、提示词/话术和 action。 - [SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_LIBRARY_DETAIL_STAGE5_EXTENSION_DESIGN_2026-04-22.md):补齐 `M5` Stage 5 遗漏的 owner-only `GET /api/runtime/custom-world-library/:profileId` 设计,冻结单条 profile detail 的 SpacetimeDB procedure、client facade、404 语义与 Axum 路由扩展方式。 - [SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md](./SPACETIMEDB_CUSTOM_WORLD_WORKS_AND_AGENT_EXTENSION_STAGE9_DESIGN_2026-04-22.md):冻结 `M5` 剩余主链的 works、card detail、publish gate、supportedActions、action registry 与 AI/OSS 兼容路由边界,作为 Stage 9 到收口阶段的统一落地依据。 - [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。 diff --git a/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md b/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md new file mode 100644 index 00000000..3dbb6e28 --- /dev/null +++ b/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md @@ -0,0 +1,126 @@ +# 统一创作品类 Agent 对话框架技术方案 + +日期:`2026-04-22` + +## 1. 目标 + +把平台内所有“先 Agent 聊天收束锚点,再生成结果页”的创作流程统一到一套前端框架: + +1. UI 交互共用一套:标题区、返回、生成结果页按钮、锚点卡片、进度条、操作横幅、聊天气泡、推荐回复、输入框。 +2. 对话进度管理共用一套:进度归一化、忙碌态判断、SSE `reply_delta / session / error` 解析、操作状态展示。 +3. 品类差异只允许落在配置和后端领域逻辑:锚点列表、提示词/占位文案、生成结果页 action、快捷补全/总结话术、结果页与运行态。 + +## 2. 本轮范围 + +覆盖当前已接入平台入口的三条创作链: + +1. RPG / Custom World Agent 创作。 +2. 大鱼吃小鱼 Agent 创作。 +3. 拼图 Agent 创作。 + +本轮不迁移结果页、运行态、发布、资产生成 UI;这些仍按各品类自己的页面承载。 + +## 3. 前端结构 + +新增目录: + +```text +src/components/creation-agent/ +├─ CreationAgentWorkspace.tsx +└─ index.ts + +src/services/creation-agent/ +├─ creationAgentProgress.ts +├─ creationAgentSse.ts +└─ index.ts +``` + +### 3.1 `CreationAgentWorkspace` + +统一组件只接收通用 view model: + +1. `session`: `CreationAgentSessionView | null` +2. `theme`: 品类主题色与背景 class +3. `primaryAction`: 生成结果页按钮配置 +4. `progressActions`: 总结、补全等可选快捷动作 +5. `activeOperation`: 可选操作状态 +6. `streamingReplyText / isStreamingReply / isBusy / error` +7. `onBack / onSubmitText / onPrimaryAction / onQuickAction` + +组件内部只做表现,不读取任何 RPG、Big Fish、Puzzle 专属字段。 + +### 3.2 会话 view model + +各品类工作区负责把自己的 session 映射成: + +1. `title` +2. `assistantSummary` +3. `progressPercent` +4. `currentTurn` +5. `anchors: { key, label, value, status }[]` +6. `messages: { id, role, kind, text, createdAt }[]` +7. `recommendedReplies` + +因此新增品类时只需要新增 mapper,不再复制聊天工作区。 + +### 3.3 进度管理 + +`creationAgentProgress.ts` 统一提供: + +1. `normalizeCreationAgentProgress(progressPercent)` +2. `isCreationAgentOperationBusy(operation)` +3. `resolveCreationAgentProgressHint(progressPercent, copy?)` +4. `resolveCreationAnchorStatusLabel(status)` +5. `createCreationAgentClientMessageId(prefix)` + +### 3.4 SSE 解析 + +`creationAgentSse.ts` 统一解析现有 SSE 事件: + +1. `reply_delta`: 调用 `onUpdate(text)` +2. `session`: 缓存最终 session +3. `error`: 抛出后端错误 +4. 流结束后若没有 session,抛出品类传入的 incomplete message + +各品类 client 只负责打开自己的 URL 和提供类型参数。 + +## 4. 品类差异边界 + +### 4.1 RPG / Custom World + +保留差异: + +1. 8 个 RPG 高杠杆锚点映射。 +2. 总结当前设定话术。 +3. 补全剩余设定话术。 +4. 生成结果页 action:`draft_foundation`。 + +### 4.2 大鱼吃小鱼 + +保留差异: + +1. 4 个玩法锚点映射。 +2. 输入框占位提示。 +3. 生成结果页 action:`big_fish_compile_draft`。 + +### 4.3 拼图 + +保留差异: + +1. 拼图视觉/题材锚点映射。 +2. 输入框占位提示。 +3. 生成结果页 action:`compile_puzzle_draft`。 + +## 5. 禁止事项 + +1. 禁止在统一组件中判断具体品类名称后写分支业务。 +2. 禁止把 Big Fish / Puzzle 的状态写入 RPG 命名脚本。 +3. 禁止把后端锚点收束、提示词生成或进度裁决搬到前端。 +4. 禁止为了统一 UI 改写结果页、运行态或发布流程。 + +## 6. 验收 + +1. 三个创作流程的 Agent 聊天区都通过 `CreationAgentWorkspace` 渲染。 +2. Big Fish 与 Puzzle 不再各自复制聊天 UI、锚点卡片、输入框和进度条。 +3. RPG / Custom World 保留原有“总结当前设定 / 补全剩余设定 / 生成游戏设定草稿”交互。 +4. 定向 TypeScript / ESLint / 编码检查通过。 diff --git a/packages/shared/src/contracts/bigFish.ts b/packages/shared/src/contracts/bigFish.ts new file mode 100644 index 00000000..f847aa24 --- /dev/null +++ b/packages/shared/src/contracts/bigFish.ts @@ -0,0 +1,190 @@ +/** + * 大鱼吃小鱼玩法域前端共享契约。 + * 字段与 server-rs/shared-contracts/src/big_fish.rs 保持 camelCase 对齐。 + */ +export type CreateBigFishSessionRequest = { + seedText?: string; +}; + +export type SendBigFishMessageRequest = { + clientMessageId: string; + text: string; +}; + +export type BigFishActionId = + | 'big_fish_compile_draft' + | 'big_fish_generate_level_main_image' + | 'big_fish_generate_level_motion' + | 'big_fish_generate_stage_background' + | 'big_fish_publish_game'; + +export type ExecuteBigFishActionRequest = { + action: BigFishActionId; + level?: number; + motionKey?: 'idle_float' | 'move_swim' | string; +}; + +export type SubmitBigFishInputRequest = { + x: number; + y: number; +}; + +export type BigFishAnchorStatus = + | 'confirmed' + | 'inferred' + | 'missing' + | 'locked' + | string; + +export type BigFishAnchorItemResponse = { + key: string; + label: string; + value: string; + status: BigFishAnchorStatus; +}; + +export type BigFishAnchorPackResponse = { + gameplayPromise: BigFishAnchorItemResponse; + ecologyVisualTheme: BigFishAnchorItemResponse; + growthLadder: BigFishAnchorItemResponse; + riskTempo: BigFishAnchorItemResponse; +}; + +export type BigFishLevelBlueprintResponse = { + level: number; + name: string; + oneLineFantasy: string; + silhouetteDirection: string; + sizeRatio: number; + visualPromptSeed: string; + motionPromptSeed: string; + mergeSourceLevel?: number | null; + preyWindow: number[]; + threatWindow: number[]; + isFinalLevel: boolean; +}; + +export type BigFishBackgroundBlueprintResponse = { + theme: string; + colorMood: string; + foregroundHints: string; + midgroundComposition: string; + backgroundDepth: string; + safePlayAreaHint: string; + spawnEdgeHint: string; + backgroundPromptSeed: string; +}; + +export type BigFishRuntimeParamsResponse = { + levelCount: number; + mergeCountPerUpgrade: number; + spawnTargetCount: number; + leaderMoveSpeed: number; + followerCatchUpSpeed: number; + offscreenCullSeconds: number; + preySpawnDeltaLevels: number[]; + threatSpawnDeltaLevels: number[]; + winLevel: number; +}; + +export type BigFishGameDraftResponse = { + title: string; + subtitle: string; + coreFun: string; + ecologyTheme: string; + levels: BigFishLevelBlueprintResponse[]; + background: BigFishBackgroundBlueprintResponse; + runtimeParams: BigFishRuntimeParamsResponse; +}; + +export type BigFishAgentMessageResponse = { + id: string; + role: 'user' | 'assistant' | string; + kind: 'chat' | 'system' | 'warning' | string; + text: string; + createdAt: string; +}; + +export type BigFishAssetKind = + | 'level_main_image' + | 'level_motion' + | 'stage_background' + | string; + +export type BigFishAssetStatus = 'empty' | 'ready' | 'generating' | string; + +export type BigFishAssetSlotResponse = { + slotId: string; + assetKind: BigFishAssetKind; + level?: number | null; + motionKey?: string | null; + status: BigFishAssetStatus; + assetUrl?: string | null; + promptSnapshot: string; + updatedAt: string; +}; + +export type BigFishAssetCoverageResponse = { + levelMainImageReadyCount: number; + levelMotionReadyCount: number; + backgroundReady: boolean; + requiredLevelCount: number; + publishReady: boolean; + blockers: string[]; +}; + +export type BigFishSessionSnapshotResponse = { + sessionId: string; + currentTurn: number; + progressPercent: number; + stage: string; + anchorPack: BigFishAnchorPackResponse; + draft?: BigFishGameDraftResponse | null; + assetSlots: BigFishAssetSlotResponse[]; + assetCoverage: BigFishAssetCoverageResponse; + messages: BigFishAgentMessageResponse[]; + lastAssistantReply?: string | null; + publishReady: boolean; + updatedAt: string; +}; + +export type BigFishSessionResponse = { + session: BigFishSessionSnapshotResponse; +}; + +export type BigFishActionResponse = { + session: BigFishSessionSnapshotResponse; +}; + +export type BigFishVector2Response = { + x: number; + y: number; +}; + +export type BigFishRuntimeEntityResponse = { + entityId: string; + level: number; + position: BigFishVector2Response; + radius: number; + offscreenSeconds: number; +}; + +export type BigFishRuntimeSnapshotResponse = { + runId: string; + sessionId: string; + status: 'running' | 'won' | 'failed' | string; + tick: number; + playerLevel: number; + winLevel: number; + leaderEntityId?: string | null; + ownedEntities: BigFishRuntimeEntityResponse[]; + wildEntities: BigFishRuntimeEntityResponse[]; + cameraCenter: BigFishVector2Response; + lastInput: BigFishVector2Response; + eventLog: string[]; + updatedAt: string; +}; + +export type BigFishRunResponse = { + run: BigFishRuntimeSnapshotResponse; +}; diff --git a/packages/shared/src/contracts/puzzleAgentActions.ts b/packages/shared/src/contracts/puzzleAgentActions.ts new file mode 100644 index 00000000..183ec46a --- /dev/null +++ b/packages/shared/src/contracts/puzzleAgentActions.ts @@ -0,0 +1,59 @@ +export type PuzzleAgentSuggestedActionType = + | 'request_summary' + | 'compile_puzzle_draft' + | 'generate_puzzle_images' + | 'publish_puzzle_work'; + +export interface PuzzleAgentSuggestedAction { + id: string; + actionType: PuzzleAgentSuggestedActionType; + label: string; +} + +export type PuzzleAgentActionType = + | 'compile_puzzle_draft' + | 'generate_puzzle_images' + | 'select_puzzle_image' + | 'publish_puzzle_work'; + +export type PuzzleAgentOperationType = + | 'process_message' + | PuzzleAgentActionType; + +export type PuzzleAgentOperationStatus = + | 'queued' + | 'running' + | 'completed' + | 'failed'; + +export interface PuzzleAgentOperationRecord { + operationId: string; + type: PuzzleAgentOperationType; + status: PuzzleAgentOperationStatus; + phaseLabel: string; + phaseDetail: string; + progress: number; + error?: string | null; +} + +export type PuzzleAgentActionRequest = + | { action: 'compile_puzzle_draft' } + | { + action: 'generate_puzzle_images'; + promptText?: string | null; + candidateCount?: number; + } + | { + action: 'select_puzzle_image'; + candidateId: string; + } + | { + action: 'publish_puzzle_work'; + levelName?: string; + summary?: string; + themeTags?: string[]; + }; + +export interface PuzzleAgentActionResponse { + operation: PuzzleAgentOperationRecord; +} diff --git a/packages/shared/src/contracts/puzzleAgentDraft.ts b/packages/shared/src/contracts/puzzleAgentDraft.ts new file mode 100644 index 00000000..b2ef92ff --- /dev/null +++ b/packages/shared/src/contracts/puzzleAgentDraft.ts @@ -0,0 +1,58 @@ +import type { JsonObject } from './common'; + +export type PuzzleAnchorStatus = + | 'missing' + | 'inferred' + | 'confirmed' + | 'locked'; + +export interface PuzzleAnchorItem { + key: string; + label: string; + value: string; + status: PuzzleAnchorStatus; +} + +export interface PuzzleAnchorPack { + themePromise: PuzzleAnchorItem; + visualSubject: PuzzleAnchorItem; + visualMood: PuzzleAnchorItem; + compositionHooks: PuzzleAnchorItem; + tagsAndForbidden: PuzzleAnchorItem; +} + +export interface PuzzleCreatorIntent { + sourceMode: 'agent_chat'; + rawMessagesSummary: string; + themePromise: string; + visualSubject: string; + visualMood: string[]; + compositionHooks: string[]; + themeTags: string[]; + forbiddenDirectives: string[]; +} + +export interface PuzzleGeneratedImageCandidate { + candidateId: string; + imageSrc: string; + assetId: string; + prompt: string; + actualPrompt?: string | null; + sourceType: 'generated' | 'uploaded'; + selected: boolean; +} + +export interface PuzzleResultDraft { + levelName: string; + summary: string; + themeTags: string[]; + forbiddenDirectives: string[]; + creatorIntent: PuzzleCreatorIntent | null; + anchorPack: PuzzleAnchorPack; + candidates: PuzzleGeneratedImageCandidate[]; + selectedCandidateId: string | null; + coverImageSrc: string | null; + coverAssetId: string | null; + generationStatus: 'idle' | 'generating' | 'ready'; + metadata?: JsonObject | null; +} diff --git a/packages/shared/src/contracts/puzzleAgentSession.ts b/packages/shared/src/contracts/puzzleAgentSession.ts new file mode 100644 index 00000000..b6af08ef --- /dev/null +++ b/packages/shared/src/contracts/puzzleAgentSession.ts @@ -0,0 +1,58 @@ +import type { PuzzleAgentActionResponse, PuzzleAgentSuggestedAction } from './puzzleAgentActions'; +import type { PuzzleAnchorPack, PuzzleResultDraft } from './puzzleAgentDraft'; +import type { PuzzleResultPreviewEnvelope } from './puzzleResultPreview'; + +export type PuzzleAgentStage = + | 'collecting_anchors' + | 'draft_ready' + | 'image_refining' + | 'ready_to_publish' + | 'published'; + +export type PuzzleAgentMessageRole = 'user' | 'assistant' | 'system'; + +export type PuzzleAgentMessageKind = + | 'chat' + | 'summary' + | 'action_result' + | 'warning'; + +export interface PuzzleAgentMessage { + id: string; + role: PuzzleAgentMessageRole; + kind: PuzzleAgentMessageKind; + text: string; + createdAt: string; +} + +export interface PuzzleAgentSessionSnapshot { + sessionId: string; + currentTurn: number; + progressPercent: number; + stage: PuzzleAgentStage; + anchorPack: PuzzleAnchorPack; + draft: PuzzleResultDraft | null; + messages: PuzzleAgentMessage[]; + lastAssistantReply: string | null; + publishedProfileId: string | null; + suggestedActions: PuzzleAgentSuggestedAction[]; + resultPreview: PuzzleResultPreviewEnvelope | null; + updatedAt: string; +} + +export interface CreatePuzzleAgentSessionRequest { + seedText?: string; +} + +export interface CreatePuzzleAgentSessionResponse { + session: PuzzleAgentSessionSnapshot; +} + +export interface SendPuzzleAgentMessageRequest { + clientMessageId: string; + text: string; +} + +export interface SendPuzzleAgentMessageResponse extends PuzzleAgentActionResponse { + session: PuzzleAgentSessionSnapshot; +} diff --git a/packages/shared/src/contracts/puzzleResultPreview.ts b/packages/shared/src/contracts/puzzleResultPreview.ts new file mode 100644 index 00000000..177152b0 --- /dev/null +++ b/packages/shared/src/contracts/puzzleResultPreview.ts @@ -0,0 +1,21 @@ +import type { PuzzleResultDraft } from './puzzleAgentDraft'; + +export interface PuzzleResultPreviewBlocker { + id: string; + code: string; + message: string; +} + +export interface PuzzleResultPreviewFinding { + id: string; + severity: 'info' | 'warning' | 'blocker'; + code: string; + message: string; +} + +export interface PuzzleResultPreviewEnvelope { + draft: PuzzleResultDraft; + blockers: PuzzleResultPreviewBlocker[]; + qualityFindings: PuzzleResultPreviewFinding[]; + publishReady: boolean; +} diff --git a/packages/shared/src/contracts/puzzleRuntimeSession.ts b/packages/shared/src/contracts/puzzleRuntimeSession.ts new file mode 100644 index 00000000..de8cf815 --- /dev/null +++ b/packages/shared/src/contracts/puzzleRuntimeSession.ts @@ -0,0 +1,74 @@ +export type PuzzleGridSize = 3 | 4; + +export interface PuzzleCellPosition { + row: number; + col: number; +} + +export interface PuzzlePieceState { + pieceId: string; + correctRow: number; + correctCol: number; + currentRow: number; + currentCol: number; + mergedGroupId: string | null; +} + +export interface PuzzleMergedGroupState { + groupId: string; + pieceIds: string[]; + occupiedCells: PuzzleCellPosition[]; +} + +export interface PuzzleBoardSnapshot { + rows: number; + cols: number; + pieces: PuzzlePieceState[]; + mergedGroups: PuzzleMergedGroupState[]; + selectedPieceId: string | null; + allTilesResolved: boolean; +} + +export interface PuzzleRuntimeLevelSnapshot { + runId: string; + levelIndex: number; + gridSize: PuzzleGridSize; + profileId: string; + levelName: string; + authorDisplayName: string; + themeTags: string[]; + coverImageSrc: string | null; + board: PuzzleBoardSnapshot; + status: 'playing' | 'cleared'; +} + +export interface PuzzleRunSnapshot { + runId: string; + entryProfileId: string; + clearedLevelCount: number; + currentLevelIndex: number; + currentGridSize: PuzzleGridSize; + playedProfileIds: string[]; + previousLevelTags: string[]; + currentLevel: PuzzleRuntimeLevelSnapshot | null; + recommendedNextProfileId: string | null; +} + +export interface StartPuzzleRunRequest { + profileId: string; +} + +export interface PuzzleRunResponse { + run: PuzzleRunSnapshot; +} + +export interface SwapPuzzlePiecesRequest { + firstPieceId: string; + secondPieceId: string; +} + +export interface DragPuzzlePieceRequest { + pieceId: string; + targetRow: number; + targetCol: number; +} diff --git a/packages/shared/src/contracts/puzzleWorkSummary.ts b/packages/shared/src/contracts/puzzleWorkSummary.ts new file mode 100644 index 00000000..0dd02f57 --- /dev/null +++ b/packages/shared/src/contracts/puzzleWorkSummary.ts @@ -0,0 +1,39 @@ +import type { JsonObject } from './common'; +import type { PuzzleAnchorPack } from './puzzleAgentDraft'; + +export type PuzzleWorkPublicationStatus = 'draft' | 'published'; + +export interface PuzzleWorkSummary { + workId: string; + profileId: string; + ownerUserId: string; + sourceSessionId?: string | null; + authorDisplayName: string; + levelName: string; + summary: string; + themeTags: string[]; + coverImageSrc: string | null; + coverAssetId?: string | null; + publicationStatus: PuzzleWorkPublicationStatus; + updatedAt: string; + publishedAt: string | null; + playCount: number; + publishReady: boolean; +} + +export interface PuzzleWorkProfile extends PuzzleWorkSummary { + anchorPack: PuzzleAnchorPack; + metadata?: JsonObject | null; +} + +export interface PuzzleWorksResponse { + items: PuzzleWorkSummary[]; +} + +export interface PuzzleWorkDetailResponse { + item: PuzzleWorkProfile; +} + +export interface PuzzleWorkMutationResponse { + item: PuzzleWorkProfile; +} diff --git a/packages/shared/src/contracts/rpgCreationFixtures.ts b/packages/shared/src/contracts/rpgCreationFixtures.ts index 171ccff9..697c2276 100644 --- a/packages/shared/src/contracts/rpgCreationFixtures.ts +++ b/packages/shared/src/contracts/rpgCreationFixtures.ts @@ -579,7 +579,7 @@ export function createRpgAgentSessionFixture(): RpgAgentSessionSnapshot { lockState: { lockedCardIds: ['world-foundation'], }, - draftProfile, + draftProfile: draftProfile as unknown as Record, messages: [ { id: 'message-1', diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index e6d6be73..70e84388 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './assets/qwenSprite'; export * from './contracts/auth'; +export type * from './contracts/bigFish'; export * from './contracts/common'; export type * from './contracts/customWorldAgent'; export * from './contracts/rpgAgentActions'; @@ -9,6 +10,12 @@ export * from './contracts/rpgAgentSession'; export * from './contracts/rpgCreationFixtures'; export * from './contracts/rpgCreationPreview'; export * from './contracts/rpgCreationWorkSummary'; +export * from './contracts/puzzleAgentActions'; +export * from './contracts/puzzleAgentDraft'; +export * from './contracts/puzzleAgentSession'; +export * from './contracts/puzzleResultPreview'; +export * from './contracts/puzzleRuntimeSession'; +export * from './contracts/puzzleWorkSummary'; export * from './contracts/rpgRuntimeChat'; export * from './contracts/rpgRuntimeQuestAssist'; export * from './contracts/rpgRuntimeStoryAction'; diff --git a/scripts/dev-node.mjs b/scripts/dev-node.mjs index 36aff1d9..3cc0f047 100644 --- a/scripts/dev-node.mjs +++ b/scripts/dev-node.mjs @@ -1,11 +1,12 @@ -import net from 'node:net'; -import path from 'node:path'; import {spawn} from 'node:child_process'; import {existsSync, readFileSync} from 'node:fs'; +import net from 'node:net'; +import path from 'node:path'; import {fileURLToPath, pathToFileURL} from 'node:url'; const repoRoot = fileURLToPath(new URL('../', import.meta.url)); const serverRoot = fileURLToPath(new URL('../server-node/', import.meta.url)); +const serverRsRoot = fileURLToPath(new URL('../server-rs/', import.meta.url)); const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url)); const serverTsxCliPath = fileURLToPath( new URL('../server-node/node_modules/tsx/dist/cli.mjs', import.meta.url), @@ -16,6 +17,8 @@ const serverTsxLoaderPath = fileURLToPath( const serverTsxLoaderUrl = pathToFileURL(serverTsxLoaderPath).href; const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url)); const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url)); +const spacetimeConfigPath = fileURLToPath(new URL('../spacetime.json', import.meta.url)); +const spacetimeLocalConfigPath = fileURLToPath(new URL('../spacetime.local.json', import.meta.url)); const bundledNodePath = fileURLToPath( new URL('../.tools/node-v22.22.2-win-x64/node.exe', import.meta.url), ); @@ -25,6 +28,11 @@ const bundledNpmCliPath = fileURLToPath( const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const DEFAULT_DEV_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:5432/genarrative'; const DEV_MEMORY_DATABASE_URL = 'pg-mem://genarrative-dev'; +const DEFAULT_RUST_API_HOST = '127.0.0.1'; +const DEFAULT_RUST_API_PORT = '3100'; +const DEFAULT_SPACETIME_SERVER_URL = 'http://127.0.0.1:3001'; +const DEFAULT_SPACETIME_DATABASE = 'genarrative-dev'; +const DEFAULT_INTERNAL_API_SECRET = 'genarrative-dev-internal-bridge'; function parseEnvContents(contents) { return contents @@ -63,6 +71,18 @@ function readEnvFile(filePath) { return parseEnvContents(readFileSync(filePath, 'utf8')); } +function readJsonFile(filePath) { + if (!existsSync(filePath)) { + return null; + } + + try { + return JSON.parse(readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + function resolveDatabaseProbeTarget(databaseUrl) { const trimmed = databaseUrl.trim(); if (!trimmed || !/^postgres(?:ql)?:\/\//u.test(trimmed)) { @@ -177,6 +197,8 @@ function prependEnvPath(envMap, nextEntry) { const exampleEnv = readEnvFile(envExamplePath); const localEnv = readEnvFile(envLocalPath); +const spacetimeConfig = readJsonFile(spacetimeConfigPath); +const spacetimeLocalConfig = readJsonFile(spacetimeLocalConfigPath); const mergedEnv = { ...exampleEnv, @@ -196,6 +218,22 @@ mergedEnv.PROJECT_ROOT = mergedEnv.PROJECT_ROOT || repoRoot; mergedEnv.NODE_SERVER_ADDR = mergedEnv.NODE_SERVER_ADDR || ':8081'; mergedEnv.NODE_SERVER_TARGET = mergedEnv.NODE_SERVER_TARGET || resolveServerTarget(mergedEnv.NODE_SERVER_ADDR); +mergedEnv.GENARRATIVE_API_HOST = + mergedEnv.GENARRATIVE_API_HOST || DEFAULT_RUST_API_HOST; +mergedEnv.GENARRATIVE_API_PORT = + mergedEnv.GENARRATIVE_API_PORT || DEFAULT_RUST_API_PORT; +mergedEnv.GENARRATIVE_API_TARGET = + mergedEnv.GENARRATIVE_API_TARGET || + `http://${mergedEnv.GENARRATIVE_API_HOST}:${mergedEnv.GENARRATIVE_API_PORT}`; +mergedEnv.GENARRATIVE_INTERNAL_API_SECRET = + mergedEnv.GENARRATIVE_INTERNAL_API_SECRET || DEFAULT_INTERNAL_API_SECRET; +mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL = + mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || DEFAULT_SPACETIME_SERVER_URL; +mergedEnv.GENARRATIVE_SPACETIME_DATABASE = + mergedEnv.GENARRATIVE_SPACETIME_DATABASE || + spacetimeLocalConfig?.database || + spacetimeConfig?.database || + DEFAULT_SPACETIME_DATABASE; mergedEnv.DATABASE_URL = mergedEnv.DATABASE_URL || DEFAULT_DEV_DATABASE_URL; mergedEnv.VITE_DEV_HOST = mergedEnv.VITE_DEV_HOST || '127.0.0.1'; @@ -205,9 +243,23 @@ mergedEnv.npm_config_scripts_prepend_node_path = 'true'; const exampleDatabaseUrl = `${exampleEnv.DATABASE_URL || ''}`.trim(); const localDatabaseUrl = `${localEnv.DATABASE_URL || ''}`.trim(); const processDatabaseUrl = `${process.env.DATABASE_URL || ''}`.trim(); +const exampleSpacetimeDatabase = `${exampleEnv.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim(); +const localSpacetimeDatabase = `${localEnv.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim(); +const processSpacetimeDatabase = `${process.env.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim(); const hasExplicitDatabaseUrl = Boolean(processDatabaseUrl) || (Boolean(localDatabaseUrl) && localDatabaseUrl !== exampleDatabaseUrl); +const hasExplicitSpacetimeDatabase = + Boolean(processSpacetimeDatabase) || + (Boolean(localSpacetimeDatabase) && localSpacetimeDatabase !== exampleSpacetimeDatabase); + +// 本地开发默认跟随仓库当前的 Spacetime 数据库名,只有显式覆盖时才尊重环境变量。 +if (!hasExplicitSpacetimeDatabase) { + mergedEnv.GENARRATIVE_SPACETIME_DATABASE = + spacetimeLocalConfig?.database || + spacetimeConfig?.database || + DEFAULT_SPACETIME_DATABASE; +} if (!hasExplicitDatabaseUrl) { const databaseProbeTarget = resolveDatabaseProbeTarget(mergedEnv.DATABASE_URL); @@ -228,6 +280,14 @@ if (!hasExplicitDatabaseUrl) { console.log(`[dev:node] PROJECT_ROOT=${mergedEnv.PROJECT_ROOT}`); console.log(`[dev:node] NODE_SERVER_ADDR=${mergedEnv.NODE_SERVER_ADDR}`); console.log(`[dev:node] NODE_SERVER_TARGET=${mergedEnv.NODE_SERVER_TARGET}`); +console.log(`[dev:node] GENARRATIVE_API_TARGET=${mergedEnv.GENARRATIVE_API_TARGET}`); +console.log('[dev:node] GENARRATIVE_INTERNAL_API_SECRET=[configured]'); +console.log( + `[dev:node] GENARRATIVE_SPACETIME_SERVER_URL=${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`, +); +console.log( + `[dev:node] GENARRATIVE_SPACETIME_DATABASE=${mergedEnv.GENARRATIVE_SPACETIME_DATABASE}`, +); console.log(`[dev:node] DATABASE_URL=${redactDatabaseUrl(mergedEnv.DATABASE_URL)}`); console.log(`[dev:node] VITE_DEV_HOST=${mergedEnv.VITE_DEV_HOST}`); console.log(`[dev:node] NODE_RUNTIME=${runtimeNodePath}`); @@ -329,6 +389,38 @@ const serverProcess = existsSync(serverTsxLoaderPath) stdio: 'inherit', }); +const rustApiProcess = process.platform === 'win32' + ? spawn( + 'powershell', + [ + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-File', + path.join(serverRsRoot, 'scripts', 'dev.ps1'), + '-ApiHost', + mergedEnv.GENARRATIVE_API_HOST, + '-Port', + mergedEnv.GENARRATIVE_API_PORT, + ], + { + cwd: repoRoot, + env: mergedEnv, + stdio: 'inherit', + }, + ) + : spawn( + 'bash', + [ + path.join(serverRsRoot, 'scripts', 'dev.sh'), + ], + { + cwd: repoRoot, + env: mergedEnv, + stdio: 'inherit', + }, + ); + const viteProcess = spawn( runtimeNodePath, [viteCliPath, '--port=3000', `--host=${mergedEnv.VITE_DEV_HOST}`], @@ -340,6 +432,7 @@ const viteProcess = spawn( ); registerChild('node server', serverProcess, () => viteProcess); +registerChild('rust api-server', rustApiProcess, () => viteProcess); registerChild('vite dev server', viteProcess, () => serverProcess); process.on('SIGINT', () => { diff --git a/server-node/src/app.ts b/server-node/src/app.ts index ddd24152..5c0b0be0 100644 --- a/server-node/src/app.ts +++ b/server-node/src/app.ts @@ -10,12 +10,14 @@ import { responseEnvelopeMiddleware } from './middleware/responseEnvelope.js'; import { createCharacterAssetRoutes } from './modules/assets/characterAssetRoutes.js'; import { createEditorRoutes } from './modules/editor/editorRoutes.js'; import { createAuthRoutes } from './routes/authRoutes.js'; +import { createBigFishProxyRoutes } from './routes/bigFishProxyRoutes.js'; +import { createCustomWorldAgentRoutes } from './routes/customWorldAgent.js'; +import { createPuzzleProxyRoutes } from './routes/puzzleProxyRoutes.js'; import { createRpgEntrySaveRoutes } from './routes/rpg-entry/rpgEntrySaveRoutes.js'; import { createRpgWorldLibraryRoutes } from './routes/rpg-entry/rpgWorldLibraryRoutes.js'; import { createRpgProfileRoutes } from './routes/rpg-profile/rpgProfileRoutes.js'; import { createRpgRuntimeAiAssistRoutes } from './routes/rpg-runtime/rpgRuntimeAiAssistRoutes.js'; import { createRpgRuntimeStoryRoutes } from './routes/rpg-runtime/rpgRuntimeStoryRoutes.js'; -import { createCustomWorldAgentRoutes } from './routes/customWorldAgent.js'; function matchesRoutePrefix( request: express.Request, @@ -212,6 +214,16 @@ export function createApp(context: AppContext) { withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.creation.agent.api' }), createCustomWorldAgentRoutes(context), ); + app.use( + '/api/runtime/big-fish', + withRouteMeta({ routeVersion: '2026-04-22', operation: 'bigFish.runtime.proxy.api' }), + createBigFishProxyRoutes(context), + ); + app.use( + '/api/runtime/puzzle', + withRouteMeta({ routeVersion: '2026-04-22', operation: 'puzzle.runtime.proxy.api' }), + createPuzzleProxyRoutes(context), + ); app.use( express.static(context.config.publicDir, { fallthrough: true, diff --git a/server-node/src/routes/bigFishProxyRoutes.ts b/server-node/src/routes/bigFishProxyRoutes.ts new file mode 100644 index 00000000..fe502aee --- /dev/null +++ b/server-node/src/routes/bigFishProxyRoutes.ts @@ -0,0 +1,329 @@ +import { Readable } from 'node:stream'; + +import { type Request, type Response,Router } from 'express'; + +import type { AppContext } from '../context.js'; +import { badRequest, upstreamError } from '../errors.js'; +import { + API_RESPONSE_ENVELOPE_HEADER, + API_VERSION_HEADER, + asyncHandler, + prepareApiResponse, + RESPONSE_TIME_HEADER, + ROUTE_VERSION_HEADER, +} from '../http.js'; +import { requireJwtAuth } from '../middleware/auth.js'; +import { routeMeta } from '../middleware/routeMeta.js'; + +const BIG_FISH_ROUTE_VERSION = '2026-04-22'; +const DEFAULT_RUST_API_TARGET = 'http://127.0.0.1:3100'; +const DEFAULT_INTERNAL_API_SECRET = 'genarrative-dev-internal-bridge'; +const INTERNAL_USER_HEADER = 'x-genarrative-authenticated-user-id'; +const INTERNAL_SECRET_HEADER = 'x-genarrative-internal-api-secret'; + +function resolveRustApiTarget(context: AppContext) { + const configured = + context.config.rawEnv.GENARRATIVE_API_TARGET?.trim() || + context.config.rawEnv.RUST_API_SERVER_TARGET?.trim() || + ''; + return configured || DEFAULT_RUST_API_TARGET; +} + +function resolveInternalApiSecret(context: AppContext) { + return ( + context.config.rawEnv.GENARRATIVE_INTERNAL_API_SECRET?.trim() || + DEFAULT_INTERNAL_API_SECRET + ); +} + +function normalizeRouteSuffix(path: string) { + const normalized = path.startsWith('/') ? path : `/${path}`; + return normalized.replace(/\/+$/u, ''); +} + +function buildUpstreamUrl(context: AppContext, pathSuffix: string) { + const baseUrl = resolveRustApiTarget(context).replace(/\/+$/u, ''); + return `${baseUrl}${normalizeRouteSuffix(pathSuffix)}`; +} + +function pickForwardHeaders( + request: Request, + context: AppContext, + userId: string, +) { + const forwardedHeaders = new Headers(); + + const contentType = request.header('content-type')?.trim(); + if (contentType) { + forwardedHeaders.set('content-type', contentType); + } + + const accept = request.header('accept')?.trim(); + if (accept) { + forwardedHeaders.set('accept', accept); + } + + const requestId = request.requestId?.trim(); + if (requestId) { + forwardedHeaders.set('x-request-id', requestId); + } + + const envelope = request.header(API_RESPONSE_ENVELOPE_HEADER)?.trim(); + if (envelope) { + forwardedHeaders.set(API_RESPONSE_ENVELOPE_HEADER, envelope); + } + + forwardedHeaders.set(INTERNAL_USER_HEADER, userId); + const internalSecret = resolveInternalApiSecret(context); + if (internalSecret) { + forwardedHeaders.set(INTERNAL_SECRET_HEADER, internalSecret); + } + return forwardedHeaders; +} + +function readBodyAllowed(method: string) { + return !['GET', 'HEAD'].includes(method.toUpperCase()); +} + +async function proxyBigFishRequest(params: { + context: AppContext; + request: Request; + response: Response; + pathSuffix: string; + streamBody?: boolean; +}) { + const { context, request, response, pathSuffix, streamBody = false } = params; + const userId = request.userId?.trim(); + if (!userId) { + throw badRequest('缺少已认证用户上下文'); + } + + const upstreamUrl = buildUpstreamUrl(context, pathSuffix); + const method = request.method.toUpperCase(); + const body = + readBodyAllowed(method) && request.body !== undefined + ? JSON.stringify(request.body) + : undefined; + + let upstreamResponse: globalThis.Response; + try { + upstreamResponse = await fetch(upstreamUrl, { + method, + // 这里显式转发“已通过 Node 校验的用户身份”,让 Big Fish 继续由 Rust 真相后端处理。 + headers: pickForwardHeaders(request, context, userId), + body, + }); + } catch (error) { + request.log?.error( + { + err: error, + user_id: userId, + upstream_url: upstreamUrl, + }, + 'big fish upstream request failed', + ); + throw upstreamError('大鱼吃小鱼后端暂时不可用'); + } + + prepareApiResponse(request, response, { + statusCode: upstreamResponse.status, + headers: { + 'Content-Type': + upstreamResponse.headers.get('content-type') || + 'application/json; charset=utf-8', + 'Cache-Control': + upstreamResponse.headers.get('cache-control') || 'no-cache', + }, + routeMeta: { + routeVersion: BIG_FISH_ROUTE_VERSION, + }, + }); + + const upstreamRequestId = upstreamResponse.headers.get('x-request-id'); + if (upstreamRequestId) { + response.setHeader('x-upstream-request-id', upstreamRequestId); + } + + const upstreamRouteVersion = upstreamResponse.headers.get(ROUTE_VERSION_HEADER); + if (upstreamRouteVersion) { + response.setHeader('x-upstream-route-version', upstreamRouteVersion); + } + + const upstreamApiVersion = upstreamResponse.headers.get(API_VERSION_HEADER); + if (upstreamApiVersion) { + response.setHeader('x-upstream-api-version', upstreamApiVersion); + } + + const upstreamLatency = upstreamResponse.headers.get(RESPONSE_TIME_HEADER); + if (upstreamLatency) { + response.setHeader('x-upstream-response-time-ms', upstreamLatency); + } + + if (streamBody) { + if (!upstreamResponse.body) { + throw upstreamError('大鱼吃小鱼流式响应不可用'); + } + + response.flushHeaders?.(); + await Readable.fromWeb(upstreamResponse.body as never).pipe(response); + return; + } + + response.end(await upstreamResponse.text()); +} + +function readParam(value: string | string[] | undefined) { + return Array.isArray(value) ? value[0]?.trim() || '' : value?.trim() || ''; +} + +export function createBigFishProxyRoutes(context: AppContext) { + const router = Router(); + const requireAuth = requireJwtAuth(context.config, context.userRepository); + + router.use(requireAuth); + + router.post( + '/agent/sessions', + routeMeta({ operation: 'runtime.bigFish.createSession', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: '/api/runtime/big-fish/agent/sessions', + }); + }), + ); + + router.get( + '/agent/sessions/:sessionId', + routeMeta({ operation: 'runtime.bigFish.getSession', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/agent/sessions/${encodeURIComponent(sessionId)}`, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/messages', + routeMeta({ operation: 'runtime.bigFish.sendMessage', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/agent/sessions/${encodeURIComponent(sessionId)}/messages`, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/messages/stream', + routeMeta({ + operation: 'runtime.bigFish.streamMessage', + routeVersion: BIG_FISH_ROUTE_VERSION, + }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/agent/sessions/${encodeURIComponent(sessionId)}/messages/stream`, + streamBody: true, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/actions', + routeMeta({ operation: 'runtime.bigFish.executeAction', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/agent/sessions/${encodeURIComponent(sessionId)}/actions`, + }); + }), + ); + + router.post( + '/sessions/:sessionId/runs', + routeMeta({ operation: 'runtime.bigFish.startRun', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/runs`, + }); + }), + ); + + router.get( + '/runs/:runId', + routeMeta({ operation: 'runtime.bigFish.getRun', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/runs/${encodeURIComponent(runId)}`, + }); + }), + ); + + router.post( + '/runs/:runId/input', + routeMeta({ operation: 'runtime.bigFish.submitInput', routeVersion: BIG_FISH_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyBigFishRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/big-fish/runs/${encodeURIComponent(runId)}/input`, + }); + }), + ); + + return router; +} diff --git a/server-node/src/routes/puzzleProxyRoutes.ts b/server-node/src/routes/puzzleProxyRoutes.ts new file mode 100644 index 00000000..6b92f2fa --- /dev/null +++ b/server-node/src/routes/puzzleProxyRoutes.ts @@ -0,0 +1,439 @@ +import { Readable } from 'node:stream'; + +import { type Request, type Response, Router } from 'express'; + +import type { AppContext } from '../context.js'; +import { badRequest, upstreamError } from '../errors.js'; +import { + API_RESPONSE_ENVELOPE_HEADER, + API_VERSION_HEADER, + asyncHandler, + prepareApiResponse, + RESPONSE_TIME_HEADER, + ROUTE_VERSION_HEADER, +} from '../http.js'; +import { requireJwtAuth } from '../middleware/auth.js'; +import { routeMeta } from '../middleware/routeMeta.js'; + +const PUZZLE_ROUTE_VERSION = '2026-04-22'; +const DEFAULT_RUST_API_TARGET = 'http://127.0.0.1:3100'; +const DEFAULT_INTERNAL_API_SECRET = 'genarrative-dev-internal-bridge'; +const INTERNAL_USER_HEADER = 'x-genarrative-authenticated-user-id'; +const INTERNAL_SECRET_HEADER = 'x-genarrative-internal-api-secret'; + +function resolveRustApiTarget(context: AppContext) { + const configured = + context.config.rawEnv.GENARRATIVE_API_TARGET?.trim() || + context.config.rawEnv.RUST_API_SERVER_TARGET?.trim() || + ''; + return configured || DEFAULT_RUST_API_TARGET; +} + +function resolveInternalApiSecret(context: AppContext) { + return ( + context.config.rawEnv.GENARRATIVE_INTERNAL_API_SECRET?.trim() || + DEFAULT_INTERNAL_API_SECRET + ); +} + +function normalizeRouteSuffix(path: string) { + const normalized = path.startsWith('/') ? path : `/${path}`; + return normalized.replace(/\/+$/u, ''); +} + +function buildUpstreamUrl(context: AppContext, pathSuffix: string) { + const baseUrl = resolveRustApiTarget(context).replace(/\/+$/u, ''); + return `${baseUrl}${normalizeRouteSuffix(pathSuffix)}`; +} + +function pickForwardHeaders( + request: Request, + context: AppContext, + userId: string, +) { + const forwardedHeaders = new Headers(); + + const contentType = request.header('content-type')?.trim(); + if (contentType) { + forwardedHeaders.set('content-type', contentType); + } + + const accept = request.header('accept')?.trim(); + if (accept) { + forwardedHeaders.set('accept', accept); + } + + const requestId = request.requestId?.trim(); + if (requestId) { + forwardedHeaders.set('x-request-id', requestId); + } + + const envelope = request.header(API_RESPONSE_ENVELOPE_HEADER)?.trim(); + if (envelope) { + forwardedHeaders.set(API_RESPONSE_ENVELOPE_HEADER, envelope); + } + + forwardedHeaders.set(INTERNAL_USER_HEADER, userId); + const internalSecret = resolveInternalApiSecret(context); + if (internalSecret) { + forwardedHeaders.set(INTERNAL_SECRET_HEADER, internalSecret); + } + return forwardedHeaders; +} + +function readBodyAllowed(method: string) { + return !['GET', 'HEAD'].includes(method.toUpperCase()); +} + +async function proxyPuzzleRequest(params: { + context: AppContext; + request: Request; + response: Response; + pathSuffix: string; + streamBody?: boolean; +}) { + const { context, request, response, pathSuffix, streamBody = false } = params; + const userId = request.userId?.trim(); + if (!userId) { + throw badRequest('缺少已认证用户上下文'); + } + + const upstreamUrl = buildUpstreamUrl(context, pathSuffix); + const method = request.method.toUpperCase(); + const body = + readBodyAllowed(method) && request.body !== undefined + ? JSON.stringify(request.body) + : undefined; + + let upstreamResponse: globalThis.Response; + try { + upstreamResponse = await fetch(upstreamUrl, { + method, + headers: pickForwardHeaders(request, context, userId), + body, + }); + } catch (error) { + request.log?.error( + { + err: error, + user_id: userId, + upstream_url: upstreamUrl, + }, + 'puzzle upstream request failed', + ); + throw upstreamError('拼图后端暂时不可用'); + } + + prepareApiResponse(request, response, { + statusCode: upstreamResponse.status, + headers: { + 'Content-Type': + upstreamResponse.headers.get('content-type') || + 'application/json; charset=utf-8', + 'Cache-Control': + upstreamResponse.headers.get('cache-control') || 'no-cache', + }, + routeMeta: { + routeVersion: PUZZLE_ROUTE_VERSION, + }, + }); + + const upstreamRequestId = upstreamResponse.headers.get('x-request-id'); + if (upstreamRequestId) { + response.setHeader('x-upstream-request-id', upstreamRequestId); + } + + const upstreamRouteVersion = upstreamResponse.headers.get(ROUTE_VERSION_HEADER); + if (upstreamRouteVersion) { + response.setHeader('x-upstream-route-version', upstreamRouteVersion); + } + + const upstreamApiVersion = upstreamResponse.headers.get(API_VERSION_HEADER); + if (upstreamApiVersion) { + response.setHeader('x-upstream-api-version', upstreamApiVersion); + } + + const upstreamLatency = upstreamResponse.headers.get(RESPONSE_TIME_HEADER); + if (upstreamLatency) { + response.setHeader('x-upstream-response-time-ms', upstreamLatency); + } + + if (streamBody) { + if (!upstreamResponse.body) { + throw upstreamError('拼图流式响应不可用'); + } + + response.flushHeaders?.(); + await Readable.fromWeb(upstreamResponse.body as never).pipe(response); + return; + } + + response.end(await upstreamResponse.text()); +} + +function readParam(value: string | string[] | undefined) { + return Array.isArray(value) ? value[0]?.trim() || '' : value?.trim() || ''; +} + +export function createPuzzleProxyRoutes(context: AppContext) { + const router = Router(); + const requireAuth = requireJwtAuth(context.config, context.userRepository); + + router.use(requireAuth); + + router.post( + '/agent/sessions', + routeMeta({ operation: 'runtime.puzzle.createSession', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: '/api/runtime/puzzle/agent/sessions', + }); + }), + ); + + router.get( + '/agent/sessions/:sessionId', + routeMeta({ operation: 'runtime.puzzle.getSession', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/agent/sessions/${encodeURIComponent(sessionId)}`, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/messages', + routeMeta({ operation: 'runtime.puzzle.sendMessage', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/agent/sessions/${encodeURIComponent(sessionId)}/messages`, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/messages/stream', + routeMeta({ + operation: 'runtime.puzzle.streamMessage', + routeVersion: PUZZLE_ROUTE_VERSION, + }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/agent/sessions/${encodeURIComponent(sessionId)}/messages/stream`, + streamBody: true, + }); + }), + ); + + router.post( + '/agent/sessions/:sessionId/actions', + routeMeta({ operation: 'runtime.puzzle.executeAction', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const sessionId = readParam(request.params.sessionId); + if (!sessionId) { + throw badRequest('sessionId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/agent/sessions/${encodeURIComponent(sessionId)}/actions`, + }); + }), + ); + + router.get( + '/works', + routeMeta({ operation: 'runtime.puzzle.listWorks', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: '/api/runtime/puzzle/works', + }); + }), + ); + + router.get( + '/works/:profileId', + routeMeta({ operation: 'runtime.puzzle.getWorkDetail', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const profileId = readParam(request.params.profileId); + if (!profileId) { + throw badRequest('profileId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/works/${encodeURIComponent(profileId)}`, + }); + }), + ); + + router.put( + '/works/:profileId', + routeMeta({ operation: 'runtime.puzzle.updateWork', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const profileId = readParam(request.params.profileId); + if (!profileId) { + throw badRequest('profileId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/works/${encodeURIComponent(profileId)}`, + }); + }), + ); + + router.get( + '/gallery', + routeMeta({ operation: 'runtime.puzzle.listGallery', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: '/api/runtime/puzzle/gallery', + }); + }), + ); + + router.get( + '/gallery/:profileId', + routeMeta({ operation: 'runtime.puzzle.getGalleryDetail', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const profileId = readParam(request.params.profileId); + if (!profileId) { + throw badRequest('profileId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/gallery/${encodeURIComponent(profileId)}`, + }); + }), + ); + + router.post( + '/runs', + routeMeta({ operation: 'runtime.puzzle.startRun', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: '/api/runtime/puzzle/runs', + }); + }), + ); + + router.get( + '/runs/:runId', + routeMeta({ operation: 'runtime.puzzle.getRun', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/runs/${encodeURIComponent(runId)}`, + }); + }), + ); + + router.post( + '/runs/:runId/swap', + routeMeta({ operation: 'runtime.puzzle.swapPieces', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/runs/${encodeURIComponent(runId)}/swap`, + }); + }), + ); + + router.post( + '/runs/:runId/drag', + routeMeta({ operation: 'runtime.puzzle.dragPiece', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/runs/${encodeURIComponent(runId)}/drag`, + }); + }), + ); + + router.post( + '/runs/:runId/next-level', + routeMeta({ operation: 'runtime.puzzle.nextLevel', routeVersion: PUZZLE_ROUTE_VERSION }), + asyncHandler(async (request, response) => { + const runId = readParam(request.params.runId); + if (!runId) { + throw badRequest('runId is required'); + } + + await proxyPuzzleRequest({ + context, + request, + response, + pathSuffix: `/api/runtime/puzzle/runs/${encodeURIComponent(runId)}/next-level`, + }); + }), + ); + + return router; +} diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index c5c1db01..fd384b59 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -78,6 +78,7 @@ dependencies = [ "module-ai", "module-assets", "module-auth", + "module-big-fish", "module-combat", "module-custom-world", "module-inventory", @@ -1425,6 +1426,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "module-big-fish" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "shared-kernel", + "spacetimedb", +] + [[package]] name = "module-combat" version = "0.1.0" @@ -1471,6 +1482,16 @@ dependencies = [ "spacetimedb", ] +[[package]] +name = "module-puzzle" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "shared-kernel", + "spacetimedb", +] + [[package]] name = "module-quest" version = "0.1.0" @@ -2487,10 +2508,12 @@ version = "0.1.0" dependencies = [ "module-ai", "module-assets", + "module-big-fish", "module-combat", "module-custom-world", "module-inventory", "module-npc", + "module-puzzle", "module-runtime", "module-runtime-item", "module-story", @@ -2507,15 +2530,18 @@ dependencies = [ "log", "module-ai", "module-assets", + "module-big-fish", "module-combat", "module-custom-world", "module-inventory", "module-npc", "module-progression", + "module-puzzle", "module-quest", "module-runtime", "module-runtime-item", "module-story", + "serde", "serde_json", "shared-kernel", "spacetimedb", diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index 843a3008..7c45d349 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -8,10 +8,12 @@ members = [ "crates/module-ai", "crates/module-assets", "crates/module-auth", + "crates/module-big-fish", "crates/module-combat", "crates/module-inventory", "crates/module-custom-world", "crates/module-npc", + "crates/module-puzzle", "crates/module-progression", "crates/module-quest", "crates/module-runtime", diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index f8748289..3f8384d1 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -11,6 +11,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus module-ai = { path = "../module-ai" } module-assets = { path = "../module-assets" } module-auth = { path = "../module-auth" } +module-big-fish = { path = "../module-big-fish" } module-combat = { path = "../module-combat" } module-custom-world = { path = "../module-custom-world" } module-inventory = { path = "../module-inventory" } diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 83d28bb8..9bddda4c 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -24,6 +24,11 @@ use crate::{ }, auth_me::auth_me, auth_sessions::auth_sessions, + big_fish::{ + create_big_fish_session, execute_big_fish_action, get_big_fish_run, get_big_fish_session, + start_big_fish_run, stream_big_fish_message, submit_big_fish_input, + submit_big_fish_message, + }, custom_world::{ create_custom_world_agent_session, execute_custom_world_agent_action, get_custom_world_agent_card_detail, @@ -48,6 +53,13 @@ use crate::{ logout_all::logout_all, password_entry::password_entry, phone_auth::{phone_login, send_phone_code}, + puzzle::{ + advance_puzzle_next_level, create_puzzle_agent_session, drag_puzzle_piece_or_group, + execute_puzzle_agent_action, get_puzzle_agent_session, get_puzzle_gallery_detail, + get_puzzle_run, get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery, + put_puzzle_work, start_puzzle_run, stream_puzzle_agent_message, + submit_puzzle_agent_message, swap_puzzle_pieces, + }, refresh_session::refresh_session, request_context::{attach_request_context, resolve_request_id}, response_headers::propagate_request_id_header, @@ -348,6 +360,153 @@ pub fn build_router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/runtime/big-fish/agent/sessions", + post(create_big_fish_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/agent/sessions/{session_id}", + get(get_big_fish_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/agent/sessions/{session_id}/messages", + post(submit_big_fish_message).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/agent/sessions/{session_id}/messages/stream", + post(stream_big_fish_message).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/agent/sessions/{session_id}/actions", + post(execute_big_fish_action).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/sessions/{session_id}/runs", + post(start_big_fish_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/runs/{run_id}", + get(get_big_fish_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/big-fish/runs/{run_id}/input", + post(submit_big_fish_input).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/agent/sessions", + post(create_puzzle_agent_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/agent/sessions/{session_id}", + get(get_puzzle_agent_session).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/agent/sessions/{session_id}/messages", + post(submit_puzzle_agent_message).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/agent/sessions/{session_id}/messages/stream", + post(stream_puzzle_agent_message).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/agent/sessions/{session_id}/actions", + post(execute_puzzle_agent_action).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/works", + get(get_puzzle_works).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/works/{profile_id}", + get(get_puzzle_work_detail) + .put(put_puzzle_work) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route("/api/runtime/puzzle/gallery", get(list_puzzle_gallery)) + .route( + "/api/runtime/puzzle/gallery/{profile_id}", + get(get_puzzle_gallery_detail), + ) + .route( + "/api/runtime/puzzle/runs", + post(start_puzzle_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/runs/{run_id}", + get(get_puzzle_run).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/runs/{run_id}/swap", + post(swap_puzzle_pieces).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/runs/{run_id}/drag", + post(drag_puzzle_piece_or_group).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/runtime/puzzle/runs/{run_id}/next-level", + post(advance_puzzle_next_level).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route( "/api/custom-world/entity", post(generate_custom_world_entity).route_layer(middleware::from_fn_with_state( diff --git a/server-rs/crates/api-server/src/auth.rs b/server-rs/crates/api-server/src/auth.rs index df2c3eac..d15d63f3 100644 --- a/server-rs/crates/api-server/src/auth.rs +++ b/server-rs/crates/api-server/src/auth.rs @@ -8,8 +8,12 @@ use axum::{ middleware::Next, response::Response, }; -use platform_auth::{AccessTokenClaims, read_refresh_session_token, verify_access_token}; +use platform_auth::{ + AccessTokenClaims, AuthProvider, BindingStatus, read_refresh_session_token, + verify_access_token, +}; use serde_json::{Value, json}; +use time::OffsetDateTime; use tracing::warn; use crate::{ @@ -17,6 +21,9 @@ use crate::{ state::AppState, }; +const INTERNAL_AUTH_USER_ID_HEADER: &str = "x-genarrative-authenticated-user-id"; +const INTERNAL_API_SECRET_HEADER: &str = "x-genarrative-internal-api-secret"; + // 统一把已校验的 claims 写入 request extensions,避免后续 handler 再次重复解析 Bearer token。 #[derive(Clone, Debug)] pub struct AuthenticatedAccessToken { @@ -53,6 +60,15 @@ pub async fn require_bearer_auth( mut request: Request, next: Next, ) -> Result { + if request.uri().path().starts_with("/api/runtime/big-fish/") + && let Some(claims) = try_build_internal_forwarded_claims(&state, request.headers()) + { + request + .extensions_mut() + .insert(AuthenticatedAccessToken::new(claims)); + return Ok(next.run(request).await); + } + let bearer_token = extract_bearer_token(request.headers())?; let request_id = request .extensions() @@ -172,13 +188,60 @@ fn extract_bearer_token(headers: &HeaderMap) -> Result { Ok(token.to_string()) } +fn try_build_internal_forwarded_claims( + state: &AppState, + headers: &HeaderMap, +) -> Option { + let expected_secret = state.config.internal_api_secret.as_ref()?.trim(); + if expected_secret.is_empty() { + return None; + } + + let provided_secret = headers + .get(INTERNAL_API_SECRET_HEADER) + .and_then(|value| value.to_str().ok()) + .map(str::trim) + .filter(|value| !value.is_empty())?; + if provided_secret != expected_secret { + return None; + } + + let user_id = headers + .get(INTERNAL_AUTH_USER_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(str::trim) + .filter(|value| !value.is_empty())? + .to_string(); + + // 这里的 claims 只服务于经 Node 已鉴权后的本地内部转发链路,避免在开发态复制整套账号仓储。 + AccessTokenClaims::from_input( + platform_auth::AccessTokenClaimsInput { + user_id: user_id.clone(), + session_id: format!("internal-forwarded-{user_id}"), + provider: AuthProvider::Password, + roles: vec!["user".to_string()], + token_version: 0, + phone_verified: false, + binding_status: BindingStatus::Active, + display_name: None, + }, + state.auth_jwt_config(), + OffsetDateTime::now_utc(), + ) + .ok() +} + #[cfg(test)] mod tests { - use super::{RefreshSessionToken, extract_bearer_token}; + use super::{ + INTERNAL_API_SECRET_HEADER, INTERNAL_AUTH_USER_ID_HEADER, RefreshSessionToken, + extract_bearer_token, try_build_internal_forwarded_claims, + }; use axum::{ http::{HeaderMap, HeaderValue, StatusCode, header::AUTHORIZATION}, response::IntoResponse, }; + use crate::{config::AppConfig, state::AppState}; #[test] fn extract_bearer_token_accepts_standard_header() { @@ -209,4 +272,26 @@ mod tests { assert_eq!(token.token(), "refresh-token-01"); } + + #[test] + fn internal_forwarded_claims_require_matching_secret() { + let mut config = AppConfig::default(); + config.internal_api_secret = Some("bridge-secret".to_string()); + let state = AppState::new(config).expect("state should build"); + let mut headers = HeaderMap::new(); + headers.insert( + INTERNAL_AUTH_USER_ID_HEADER, + HeaderValue::from_static("user_forwarded_01"), + ); + headers.insert( + INTERNAL_API_SECRET_HEADER, + HeaderValue::from_static("bridge-secret"), + ); + + let claims = + try_build_internal_forwarded_claims(&state, &headers).expect("claims should resolve"); + + assert_eq!(claims.user_id(), "user_forwarded_01"); + assert_eq!(claims.token_version(), 0); + } } diff --git a/server-rs/crates/api-server/src/big_fish.rs b/server-rs/crates/api-server/src/big_fish.rs new file mode 100644 index 00000000..5688162b --- /dev/null +++ b/server-rs/crates/api-server/src/big_fish.rs @@ -0,0 +1,653 @@ +use axum::{ + Json, + extract::{Extension, Path, State, rejection::JsonRejection}, + http::{HeaderName, StatusCode, header}, + response::{IntoResponse, Response}, +}; +use serde_json::{Value, json}; +use shared_contracts::big_fish::{ + BigFishActionResponse, BigFishAgentMessageResponse, BigFishAnchorItemResponse, + BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse, + BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse, + BigFishRuntimeEntityResponse, BigFishRuntimeParamsResponse, BigFishRuntimeSnapshotResponse, + BigFishRunResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse, + BigFishVector2Response, CreateBigFishSessionRequest, ExecuteBigFishActionRequest, + SendBigFishMessageRequest, SubmitBigFishInputRequest, +}; +use shared_kernel::build_prefixed_uuid_id; +use spacetime_client::{ + BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord, + BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord, + BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord, BigFishLevelBlueprintRecord, + BigFishMessageSubmitRecordInput, BigFishRunInputSubmitRecordInput, BigFishRunStartRecordInput, + BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRecord, + BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record, + SpacetimeClientError, +}; + +use crate::{ + api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError, + request_context::RequestContext, state::AppState, +}; + +pub async fn create_big_fish_session( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + big_fish_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": error.body_text(), + })), + ) + })?; + + let seed_text = payload.seed_text.unwrap_or_default().trim().to_string(); + let session = state + .spacetime_client() + .create_big_fish_session(BigFishSessionCreateRecordInput { + session_id: build_prefixed_uuid_id("big-fish-session-"), + owner_user_id: authenticated.claims().user_id().to_string(), + seed_text: seed_text.clone(), + welcome_message_id: build_prefixed_uuid_id("big-fish-message-"), + welcome_message_text: build_big_fish_welcome_text(&seed_text), + created_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishSessionResponse { + session: map_big_fish_session_response(session), + }, + )) +} + +pub async fn get_big_fish_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 session = state + .spacetime_client() + .get_big_fish_session(session_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishSessionResponse { + session: map_big_fish_session_response(session), + }, + )) +} + +pub async fn submit_big_fish_message( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + big_fish_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, &session_id, "sessionId")?; + + let client_message_id = payload.client_message_id.trim().to_string(); + let message_text = payload.text.trim().to_string(); + if client_message_id.is_empty() || message_text.is_empty() { + return Err(big_fish_bad_request( + &request_context, + "clientMessageId and text are required", + )); + } + + let session = state + .spacetime_client() + .submit_big_fish_message(BigFishMessageSubmitRecordInput { + session_id, + owner_user_id: authenticated.claims().user_id().to_string(), + user_message_id: client_message_id, + user_message_text: message_text, + assistant_message_id: build_prefixed_uuid_id("big-fish-message-"), + submitted_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishSessionResponse { + session: map_big_fish_session_response(session), + }, + )) +} + +pub async fn stream_big_fish_message( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result { + let Json(payload) = payload.map_err(|error| { + big_fish_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, &session_id, "sessionId")?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let session = state + .spacetime_client() + .submit_big_fish_message(BigFishMessageSubmitRecordInput { + session_id, + owner_user_id, + user_message_id: payload.client_message_id.trim().to_string(), + user_message_text: payload.text.trim().to_string(), + assistant_message_id: build_prefixed_uuid_id("big-fish-message-"), + submitted_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + let session_response = map_big_fish_session_response(session); + let reply_text = session_response + .last_assistant_reply + .clone() + .unwrap_or_else(|| "锚点已更新。".to_string()); + let mut sse_body = String::new(); + append_sse_event(&request_context, &mut sse_body, "reply_delta", &json!({ "text": reply_text }))?; + append_sse_event(&request_context, &mut sse_body, "session", &json!({ "session": session_response }))?; + append_sse_event(&request_context, &mut sse_body, "done", &json!({ "ok": true }))?; + Ok(build_event_stream_response(sse_body)) +} + +pub async fn execute_big_fish_action( + State(state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + big_fish_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, &session_id, "sessionId")?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let now = current_utc_micros(); + let session = match payload.action.trim() { + "big_fish_compile_draft" => { + state + .spacetime_client() + .compile_big_fish_draft(session_id, owner_user_id, now) + .await + } + "big_fish_generate_level_main_image" => { + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + session_id, + owner_user_id, + asset_kind: "level_main_image".to_string(), + level: payload.level, + motion_key: None, + generated_at_micros: now, + }) + .await + } + "big_fish_generate_level_motion" => { + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + session_id, + owner_user_id, + asset_kind: "level_motion".to_string(), + level: payload.level, + motion_key: payload.motion_key, + generated_at_micros: now, + }) + .await + } + "big_fish_generate_stage_background" => { + state + .spacetime_client() + .generate_big_fish_asset(BigFishAssetGenerateRecordInput { + session_id, + owner_user_id, + asset_kind: "stage_background".to_string(), + level: None, + motion_key: None, + generated_at_micros: now, + }) + .await + } + "big_fish_publish_game" => { + state + .spacetime_client() + .publish_big_fish_game(session_id, owner_user_id, now) + .await + } + other => { + return Err(big_fish_bad_request( + &request_context, + format!("action `{other}` is not supported").as_str(), + )); + } + } + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishActionResponse { + session: map_big_fish_session_response(session), + }, + )) +} + +pub async fn start_big_fish_run( + 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 run = state + .spacetime_client() + .start_big_fish_run(BigFishRunStartRecordInput { + run_id: build_prefixed_uuid_id("big-fish-run-"), + session_id, + owner_user_id: authenticated.claims().user_id().to_string(), + started_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishRunResponse { + run: map_big_fish_runtime_response(run), + }, + )) +} + +pub async fn get_big_fish_run( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, &run_id, "runId")?; + + let run = state + .spacetime_client() + .get_big_fish_run(run_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishRunResponse { + run: map_big_fish_runtime_response(run), + }, + )) +} + +pub async fn submit_big_fish_input( + State(state): State, + Path(run_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + big_fish_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, &run_id, "runId")?; + + let run = state + .spacetime_client() + .submit_big_fish_input(BigFishRunInputSubmitRecordInput { + run_id, + owner_user_id: authenticated.claims().user_id().to_string(), + input_x: payload.x, + input_y: payload.y, + submitted_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| big_fish_error_response(&request_context, map_big_fish_client_error(error)))?; + + Ok(json_success_body( + Some(&request_context), + BigFishRunResponse { + run: map_big_fish_runtime_response(run), + }, + )) +} + +fn map_big_fish_session_response(session: BigFishSessionRecord) -> BigFishSessionSnapshotResponse { + BigFishSessionSnapshotResponse { + session_id: session.session_id, + current_turn: session.current_turn, + progress_percent: session.progress_percent, + stage: session.stage, + anchor_pack: map_big_fish_anchor_pack_response(session.anchor_pack), + draft: session.draft.map(map_big_fish_draft_response), + asset_slots: session + .asset_slots + .into_iter() + .map(map_big_fish_asset_slot_response) + .collect(), + asset_coverage: map_big_fish_asset_coverage_response(session.asset_coverage), + messages: session + .messages + .into_iter() + .map(map_big_fish_agent_message_response) + .collect(), + last_assistant_reply: session.last_assistant_reply, + publish_ready: session.publish_ready, + updated_at: session.updated_at, + } +} + +fn map_big_fish_anchor_pack_response(anchor_pack: BigFishAnchorPackRecord) -> BigFishAnchorPackResponse { + BigFishAnchorPackResponse { + gameplay_promise: map_big_fish_anchor_item_response(anchor_pack.gameplay_promise), + ecology_visual_theme: map_big_fish_anchor_item_response(anchor_pack.ecology_visual_theme), + growth_ladder: map_big_fish_anchor_item_response(anchor_pack.growth_ladder), + risk_tempo: map_big_fish_anchor_item_response(anchor_pack.risk_tempo), + } +} + +fn map_big_fish_anchor_item_response(anchor: BigFishAnchorItemRecord) -> BigFishAnchorItemResponse { + BigFishAnchorItemResponse { + key: anchor.key, + label: anchor.label, + value: anchor.value, + status: anchor.status, + } +} + +fn map_big_fish_draft_response(draft: BigFishGameDraftRecord) -> BigFishGameDraftResponse { + BigFishGameDraftResponse { + title: draft.title, + subtitle: draft.subtitle, + core_fun: draft.core_fun, + ecology_theme: draft.ecology_theme, + levels: draft + .levels + .into_iter() + .map(map_big_fish_level_response) + .collect(), + background: map_big_fish_background_response(draft.background), + runtime_params: map_big_fish_runtime_params_response(draft.runtime_params), + } +} + +fn map_big_fish_level_response(level: BigFishLevelBlueprintRecord) -> BigFishLevelBlueprintResponse { + BigFishLevelBlueprintResponse { + level: level.level, + name: level.name, + one_line_fantasy: level.one_line_fantasy, + silhouette_direction: level.silhouette_direction, + size_ratio: level.size_ratio, + visual_prompt_seed: level.visual_prompt_seed, + motion_prompt_seed: level.motion_prompt_seed, + merge_source_level: level.merge_source_level, + prey_window: level.prey_window, + threat_window: level.threat_window, + is_final_level: level.is_final_level, + } +} + +fn map_big_fish_background_response( + background: BigFishBackgroundBlueprintRecord, +) -> BigFishBackgroundBlueprintResponse { + BigFishBackgroundBlueprintResponse { + theme: background.theme, + color_mood: background.color_mood, + foreground_hints: background.foreground_hints, + midground_composition: background.midground_composition, + background_depth: background.background_depth, + safe_play_area_hint: background.safe_play_area_hint, + spawn_edge_hint: background.spawn_edge_hint, + background_prompt_seed: background.background_prompt_seed, + } +} + +fn map_big_fish_runtime_params_response( + params: BigFishRuntimeParamsRecord, +) -> BigFishRuntimeParamsResponse { + BigFishRuntimeParamsResponse { + level_count: params.level_count, + merge_count_per_upgrade: params.merge_count_per_upgrade, + spawn_target_count: params.spawn_target_count, + leader_move_speed: params.leader_move_speed, + follower_catch_up_speed: params.follower_catch_up_speed, + offscreen_cull_seconds: params.offscreen_cull_seconds, + prey_spawn_delta_levels: params.prey_spawn_delta_levels, + threat_spawn_delta_levels: params.threat_spawn_delta_levels, + win_level: params.win_level, + } +} + +fn map_big_fish_asset_slot_response(slot: BigFishAssetSlotRecord) -> BigFishAssetSlotResponse { + BigFishAssetSlotResponse { + slot_id: slot.slot_id, + asset_kind: slot.asset_kind, + level: slot.level, + motion_key: slot.motion_key, + status: slot.status, + asset_url: slot.asset_url, + prompt_snapshot: slot.prompt_snapshot, + updated_at: slot.updated_at, + } +} + +fn map_big_fish_asset_coverage_response( + coverage: BigFishAssetCoverageRecord, +) -> BigFishAssetCoverageResponse { + BigFishAssetCoverageResponse { + level_main_image_ready_count: coverage.level_main_image_ready_count, + level_motion_ready_count: coverage.level_motion_ready_count, + background_ready: coverage.background_ready, + required_level_count: coverage.required_level_count, + publish_ready: coverage.publish_ready, + blockers: coverage.blockers, + } +} + +fn map_big_fish_agent_message_response( + message: BigFishAgentMessageRecord, +) -> BigFishAgentMessageResponse { + BigFishAgentMessageResponse { + id: message.message_id, + role: message.role, + kind: message.kind, + text: message.text, + created_at: message.created_at, + } +} + +fn map_big_fish_runtime_response(run: BigFishRuntimeRecord) -> BigFishRuntimeSnapshotResponse { + BigFishRuntimeSnapshotResponse { + run_id: run.run_id, + session_id: run.session_id, + status: run.status, + tick: run.tick, + player_level: run.player_level, + win_level: run.win_level, + leader_entity_id: run.leader_entity_id, + owned_entities: run + .owned_entities + .into_iter() + .map(map_big_fish_entity_response) + .collect(), + wild_entities: run + .wild_entities + .into_iter() + .map(map_big_fish_entity_response) + .collect(), + camera_center: map_big_fish_vector_response(run.camera_center), + last_input: map_big_fish_vector_response(run.last_input), + event_log: run.event_log, + updated_at: run.updated_at, + } +} + +fn map_big_fish_entity_response(entity: BigFishRuntimeEntityRecord) -> BigFishRuntimeEntityResponse { + BigFishRuntimeEntityResponse { + entity_id: entity.entity_id, + level: entity.level, + position: map_big_fish_vector_response(entity.position), + radius: entity.radius, + offscreen_seconds: entity.offscreen_seconds, + } +} + +fn map_big_fish_vector_response(vector: BigFishVector2Record) -> BigFishVector2Response { + BigFishVector2Response { + x: vector.x, + y: vector.y, + } +} + +fn build_big_fish_welcome_text(seed_text: &str) -> String { + if seed_text.trim().is_empty() { + return "我会先帮你确定大鱼吃小鱼的核心锚点。可以从主题生态、成长阶梯或风险节奏开始。".to_string(); + } + "我已经收到你的玩法起点,会先把它整理成锚点并准备结果页草稿。".to_string() +} + +fn ensure_non_empty( + request_context: &RequestContext, + value: &str, + field_name: &str, +) -> Result<(), Response> { + if value.trim().is_empty() { + return Err(big_fish_bad_request( + request_context, + format!("{field_name} is required").as_str(), + )); + } + Ok(()) +} + +fn big_fish_bad_request(request_context: &RequestContext, message: &str) -> Response { + big_fish_error_response( + request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "big-fish", + "message": message, + })), + ) +} + +fn append_sse_event( + request_context: &RequestContext, + body: &mut String, + event: &str, + payload: &Value, +) -> Result<(), Response> { + let payload_text = serde_json::to_string(payload).map_err(|error| { + big_fish_error_response( + request_context, + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": "big-fish", + "message": format!("SSE payload 序列化失败:{error}"), + })), + ) + })?; + body.push_str("event: "); + body.push_str(event); + body.push('\n'); + body.push_str("data: "); + body.push_str(&payload_text); + body.push_str("\n\n"); + Ok(()) +} + +fn build_event_stream_response(body: String) -> Response { + ( + [ + (header::CONTENT_TYPE, "text/event-stream; charset=utf-8"), + (header::CACHE_CONTROL, "no-cache"), + (HeaderName::from_static("x-accel-buffering"), "no"), + ], + body, + ) + .into_response() +} + +fn map_big_fish_client_error(error: SpacetimeClientError) -> AppError { + let status = match &error { + SpacetimeClientError::Procedure(message) + if message.contains("big_fish_creation_session 不存在") + || message.contains("big_fish_runtime_run 不存在") => + { + StatusCode::NOT_FOUND + } + SpacetimeClientError::Procedure(message) + if message.contains("不能为空") + || message.contains("尚未编译") + || message.contains("不允许") + || message.contains("非法") + || message.contains("缺少") => + { + StatusCode::BAD_REQUEST + } + SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST, + _ => StatusCode::BAD_GATEWAY, + }; + + AppError::from_status(status).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn big_fish_error_response(request_context: &RequestContext, error: AppError) -> Response { + error.into_response_with_context(Some(request_context)) +} + +fn current_utc_micros() -> i64 { + use std::time::{SystemTime, UNIX_EPOCH}; + + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock should be after unix epoch"); + i64::try_from(duration.as_micros()).expect("current unix micros should fit in i64") +} diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 577bc7a9..cd3c6b38 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -6,6 +6,7 @@ use platform_llm::{ }; const DEFAULT_LLM_MODEL: &str = "doubao-1-5-pro-32k-character-250715"; +const DEFAULT_INTERNAL_API_SECRET: &str = "genarrative-dev-internal-bridge"; // 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。 #[derive(Clone, Debug)] @@ -13,6 +14,7 @@ pub struct AppConfig { pub bind_host: String, pub bind_port: u16, pub log_filter: String, + pub internal_api_secret: Option, pub jwt_issuer: String, pub jwt_secret: String, pub jwt_access_token_ttl_seconds: u64, @@ -62,6 +64,7 @@ impl Default for AppConfig { bind_host: "127.0.0.1".to_string(), bind_port: 3000, log_filter: "info,tower_http=info".to_string(), + internal_api_secret: Some(DEFAULT_INTERNAL_API_SECRET.to_string()), jwt_issuer: "https://auth.genarrative.local".to_string(), jwt_secret: "genarrative-dev-secret".to_string(), jwt_access_token_ttl_seconds: 2 * 60 * 60, @@ -130,6 +133,9 @@ impl AppConfig { config.log_filter = log_filter; } + config.internal_api_secret = + read_first_non_empty_env(&["GENARRATIVE_INTERNAL_API_SECRET"]); + if let Some(jwt_issuer) = read_first_non_empty_env(&["GENARRATIVE_JWT_ISSUER", "JWT_ISSUER"]) { diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index f40f0446..7832e957 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -6,6 +6,7 @@ mod auth; mod auth_me; mod auth_session; mod auth_sessions; +mod big_fish; mod config; mod custom_world; mod custom_world_ai; @@ -18,6 +19,7 @@ mod logout; mod logout_all; mod password_entry; mod phone_auth; +mod puzzle; mod refresh_session; mod request_context; mod response_headers; diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs new file mode 100644 index 00000000..4b252d6d --- /dev/null +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -0,0 +1,1394 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use axum::{ + Json, + extract::{Extension, Path as AxumPath, State, rejection::JsonRejection}, + http::{HeaderName, StatusCode, header}, + response::{IntoResponse, Response}, +}; +use serde_json::{Value, json}; +use shared_contracts::{ + puzzle_agent::{ + CreatePuzzleAgentSessionRequest, ExecutePuzzleAgentActionRequest, + PuzzleAgentActionResponse, PuzzleAgentMessageResponse, PuzzleAgentOperationResponse, + PuzzleAgentSessionResponse, PuzzleAgentSessionSnapshotResponse, + PuzzleAgentSuggestedActionResponse, PuzzleAnchorItemResponse, PuzzleAnchorPackResponse, + PuzzleCreatorIntentResponse, PuzzleGeneratedImageCandidateResponse, + PuzzleResultDraftResponse, PuzzleResultPreviewBlockerResponse, + PuzzleResultPreviewEnvelopeResponse, PuzzleResultPreviewFindingResponse, + SendPuzzleAgentMessageRequest, + }, + puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse}, + puzzle_runtime::{ + DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse, + PuzzleMergedGroupStateResponse, PuzzlePieceStateResponse, PuzzleRunResponse, + PuzzleRunSnapshotResponse, PuzzleRuntimeLevelSnapshotResponse, StartPuzzleRunRequest, + SwapPuzzlePiecesRequest, + }, + puzzle_works::{ + PuzzleWorkDetailResponse, PuzzleWorkMutationResponse, PuzzleWorkProfileResponse, + PuzzleWorkSummaryResponse, PuzzleWorksResponse, PutPuzzleWorkRequest, + }, +}; +use shared_kernel::build_prefixed_uuid_id; +use spacetime_client::{ + PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentSessionCreateRecordInput, + PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, + PuzzleAnchorPackRecord, PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord, + PuzzleGeneratedImagesSaveRecordInput, PuzzlePublishRecordInput, PuzzleResultDraftRecord, + PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, + PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, + PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput, + SpacetimeClientError, +}; + +use crate::{ + api_response::json_success_body, + auth::AuthenticatedAccessToken, + http_error::AppError, + request_context::RequestContext, + state::AppState, +}; + +const PUZZLE_AGENT_API_BASE_PROVIDER: &str = "puzzle-agent"; +const PUZZLE_WORKS_PROVIDER: &str = "puzzle-works"; +const PUZZLE_GALLERY_PROVIDER: &str = "puzzle-gallery"; +const PUZZLE_RUNTIME_PROVIDER: &str = "puzzle-runtime"; + +pub async fn create_puzzle_agent_session( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + + let seed_text = payload.seed_text.unwrap_or_default().trim().to_string(); + let session = state + .spacetime_client() + .create_puzzle_agent_session(PuzzleAgentSessionCreateRecordInput { + session_id: build_prefixed_uuid_id("puzzle-session-"), + owner_user_id: authenticated.claims().user_id().to_string(), + seed_text: seed_text.clone(), + welcome_message_id: build_prefixed_uuid_id("puzzle-message-"), + welcome_message_text: build_puzzle_welcome_text(&seed_text), + created_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleAgentSessionResponse { + session: map_puzzle_agent_session_response(session), + }, + )) +} + +pub async fn get_puzzle_agent_session( + State(state): State, + AxumPath(session_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?; + + let session = state + .spacetime_client() + .get_puzzle_agent_session( + session_id, + authenticated.claims().user_id().to_string(), + ) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleAgentSessionResponse { + session: map_puzzle_agent_session_response(session), + }, + )) +} + +pub async fn submit_puzzle_agent_message( + State(state): State, + AxumPath(session_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?; + + let client_message_id = payload.client_message_id.trim().to_string(); + let message_text = payload.text.trim().to_string(); + if client_message_id.is_empty() || message_text.is_empty() { + return Err(puzzle_bad_request( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + "clientMessageId and text are required", + )); + } + + let session = state + .spacetime_client() + .submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput { + session_id, + owner_user_id: authenticated.claims().user_id().to_string(), + user_message_id: client_message_id, + user_message_text: message_text, + submitted_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleAgentSessionResponse { + session: map_puzzle_agent_session_response(session), + }, + )) +} + +pub async fn stream_puzzle_agent_message( + State(state): State, + AxumPath(session_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?; + + let session = state + .spacetime_client() + .submit_puzzle_agent_message(PuzzleAgentMessageSubmitRecordInput { + session_id, + owner_user_id: authenticated.claims().user_id().to_string(), + user_message_id: payload.client_message_id.trim().to_string(), + user_message_text: payload.text.trim().to_string(), + submitted_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + let session_response = map_puzzle_agent_session_response(session); + let reply_text = session_response + .last_assistant_reply + .clone() + .unwrap_or_else(|| "拼图锚点已更新。".to_string()); + let mut sse_body = String::new(); + append_sse_event( + &request_context, + &mut sse_body, + "reply_delta", + &json!({ "text": reply_text }), + )?; + append_sse_event( + &request_context, + &mut sse_body, + "session", + &json!({ "session": session_response }), + )?; + append_sse_event(&request_context, &mut sse_body, "done", &json!({ "ok": true }))?; + Ok(build_event_stream_response(sse_body)) +} + +pub async fn execute_puzzle_agent_action( + State(state): State, + AxumPath(session_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, &session_id, "sessionId")?; + + let owner_user_id = authenticated.claims().user_id().to_string(); + let now = current_utc_micros(); + + let (operation_type, phase_label, phase_detail, session) = match payload.action.trim() { + "compile_puzzle_draft" => { + let session = state + .spacetime_client() + .compile_puzzle_agent_draft(session_id, owner_user_id, now) + .await; + ( + "compile_puzzle_draft", + "结果页草稿", + "已根据当前锚点编译结果页草稿。", + session, + ) + } + "generate_puzzle_images" => { + let session = state + .spacetime_client() + .get_puzzle_agent_session(session_id.clone(), owner_user_id.clone()) + .await; + let session = match session { + Ok(session) => { + let draft = session.draft.clone().ok_or_else(|| { + SpacetimeClientError::Runtime("拼图结果页草稿尚未生成".to_string()) + }); + match draft { + Ok(draft) => { + let prompt = payload + .prompt_text + .clone() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| draft.summary.clone()); + let candidate_count = payload.candidate_count.unwrap_or(2).clamp(1, 2); + let candidates = build_placeholder_puzzle_candidates( + &session.session_id, + &draft.level_name, + &prompt, + candidate_count, + ) + .map_err(SpacetimeClientError::Runtime); + match candidates { + Ok(candidates) => { + let candidates_json = serde_json::to_string( + &candidates + .iter() + .map(|candidate| { + json!({ + "candidateId": candidate.candidate_id, + "imageSrc": candidate.image_src, + "assetId": candidate.asset_id, + "prompt": candidate.prompt, + "actualPrompt": candidate.actual_prompt, + "sourceType": candidate.source_type, + "selected": candidate.selected, + }) + }) + .collect::>(), + ) + .map_err(|error| { + SpacetimeClientError::Runtime(format!( + "拼图候选图序列化失败:{error}" + )) + }); + match candidates_json { + Ok(candidates_json) => { + state + .spacetime_client() + .save_puzzle_generated_images( + PuzzleGeneratedImagesSaveRecordInput { + session_id: session.session_id, + owner_user_id, + candidates_json, + saved_at_micros: now, + }, + ) + .await + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + } + } + Err(error) => Err(error), + }; + ( + "generate_puzzle_images", + "候选图生成", + "已生成 2 张候选拼图图像。", + session, + ) + } + "select_puzzle_image" => { + let candidate_id = payload + .candidate_id + .clone() + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + puzzle_bad_request( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + "candidateId is required", + ) + })?; + let session = state + .spacetime_client() + .select_puzzle_cover_image(PuzzleSelectCoverImageRecordInput { + session_id: session_id.clone(), + owner_user_id: owner_user_id.clone(), + candidate_id, + selected_at_micros: now, + }) + .await; + ( + "select_puzzle_image", + "正式图确认", + "已应用正式拼图图片。", + session, + ) + } + "publish_puzzle_work" => { + let profile = state + .spacetime_client() + .publish_puzzle_work(PuzzlePublishRecordInput { + session_id: session_id.clone(), + owner_user_id: owner_user_id.clone(), + work_id: build_prefixed_uuid_id("puzzle-work-"), + profile_id: build_prefixed_uuid_id("puzzle-profile-"), + author_display_name: resolve_author_display_name(&state, &authenticated), + level_name: payload.level_name.clone(), + summary: payload.summary.clone(), + theme_tags: payload.theme_tags.clone(), + published_at_micros: now, + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + return Ok(json_success_body( + Some(&request_context), + PuzzleAgentActionResponse { + operation: PuzzleAgentOperationResponse { + operation_id: profile.profile_id.clone(), + operation_type: "publish_puzzle_work".to_string(), + status: "completed".to_string(), + phase_label: "作品发布".to_string(), + phase_detail: "拼图作品已发布到广场。".to_string(), + progress: 100, + error: None, + }, + }, + )); + } + other => { + return Err(puzzle_bad_request( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + format!("action `{other}` is not supported").as_str(), + )); + } + }; + + let session = session.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleAgentActionResponse { + operation: PuzzleAgentOperationResponse { + operation_id: session.session_id.clone(), + operation_type: operation_type.to_string(), + status: "completed".to_string(), + phase_label: phase_label.to_string(), + phase_detail: phase_detail.to_string(), + progress: 100, + error: None, + }, + }, + )) +} + +pub async fn get_puzzle_works( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + let items = state + .spacetime_client() + .list_puzzle_works(authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_WORKS_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleWorksResponse { + items: items + .into_iter() + .map(map_puzzle_work_summary_response) + .collect(), + }, + )) +} + +pub async fn get_puzzle_work_detail( + State(state): State, + AxumPath(profile_id): AxumPath, + Extension(request_context): Extension, + Extension(_authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, PUZZLE_WORKS_PROVIDER, &profile_id, "profileId")?; + + let item = state + .spacetime_client() + .get_puzzle_work_detail(profile_id) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_WORKS_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleWorkDetailResponse { + item: map_puzzle_work_profile_response(item), + }, + )) +} + +pub async fn put_puzzle_work( + State(state): State, + AxumPath(profile_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_WORKS_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_WORKS_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_WORKS_PROVIDER, &profile_id, "profileId")?; + + let item = state + .spacetime_client() + .update_puzzle_work(PuzzleWorkUpsertRecordInput { + profile_id, + owner_user_id: authenticated.claims().user_id().to_string(), + level_name: payload.level_name, + summary: payload.summary, + theme_tags: payload.theme_tags, + cover_image_src: payload.cover_image_src, + cover_asset_id: payload.cover_asset_id, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_WORKS_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleWorkMutationResponse { + item: map_puzzle_work_profile_response(item), + }, + )) +} + +pub async fn list_puzzle_gallery( + State(state): State, + Extension(request_context): Extension, +) -> Result, Response> { + let items = state + .spacetime_client() + .list_puzzle_gallery() + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_GALLERY_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleGalleryResponse { + items: items + .into_iter() + .map(map_puzzle_work_summary_response) + .collect(), + }, + )) +} + +pub async fn get_puzzle_gallery_detail( + State(state): State, + AxumPath(profile_id): AxumPath, + Extension(request_context): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, PUZZLE_GALLERY_PROVIDER, &profile_id, "profileId")?; + + let item = state + .spacetime_client() + .get_puzzle_gallery_detail(profile_id) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_GALLERY_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleGalleryDetailResponse { + item: map_puzzle_work_summary_response(item), + }, + )) +} + +pub async fn start_puzzle_run( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &payload.profile_id, "profileId")?; + + let run = state + .spacetime_client() + .start_puzzle_run(PuzzleRunStartRecordInput { + run_id: build_prefixed_uuid_id("puzzle-run-"), + owner_user_id: authenticated.claims().user_id().to_string(), + profile_id: payload.profile_id, + started_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleRunResponse { + run: map_puzzle_run_response(run), + }, + )) +} + +pub async fn get_puzzle_run( + State(state): State, + AxumPath(run_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?; + + let run = state + .spacetime_client() + .get_puzzle_run(run_id, authenticated.claims().user_id().to_string()) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleRunResponse { + run: map_puzzle_run_response(run), + }, + )) +} + +pub async fn swap_puzzle_pieces( + State(state): State, + AxumPath(run_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?; + ensure_non_empty( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + &payload.first_piece_id, + "firstPieceId", + )?; + ensure_non_empty( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + &payload.second_piece_id, + "secondPieceId", + )?; + + let run = state + .spacetime_client() + .swap_puzzle_pieces(PuzzleRunSwapRecordInput { + run_id, + owner_user_id: authenticated.claims().user_id().to_string(), + first_piece_id: payload.first_piece_id, + second_piece_id: payload.second_piece_id, + swapped_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleRunResponse { + run: map_puzzle_run_response(run), + }, + )) +} + +pub async fn drag_puzzle_piece_or_group( + State(state): State, + AxumPath(run_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let Json(payload) = payload.map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": PUZZLE_RUNTIME_PROVIDER, + "message": error.body_text(), + })), + ) + })?; + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?; + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &payload.piece_id, "pieceId")?; + + let run = state + .spacetime_client() + .drag_puzzle_piece_or_group(PuzzleRunDragRecordInput { + run_id, + owner_user_id: authenticated.claims().user_id().to_string(), + piece_id: payload.piece_id, + target_row: payload.target_row, + target_col: payload.target_col, + dragged_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleRunResponse { + run: map_puzzle_run_response(run), + }, + )) +} + +pub async fn advance_puzzle_next_level( + State(state): State, + AxumPath(run_id): AxumPath, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, Response> { + ensure_non_empty(&request_context, PUZZLE_RUNTIME_PROVIDER, &run_id, "runId")?; + + let run = state + .spacetime_client() + .advance_puzzle_next_level(spacetime_client::PuzzleRunNextLevelRecordInput { + run_id, + owner_user_id: authenticated.claims().user_id().to_string(), + advanced_at_micros: current_utc_micros(), + }) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_RUNTIME_PROVIDER, + map_puzzle_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + PuzzleRunResponse { + run: map_puzzle_run_response(run), + }, + )) +} + +fn map_puzzle_agent_session_response( + session: PuzzleAgentSessionRecord, +) -> PuzzleAgentSessionSnapshotResponse { + PuzzleAgentSessionSnapshotResponse { + session_id: session.session_id, + current_turn: session.current_turn, + progress_percent: session.progress_percent, + stage: session.stage, + anchor_pack: map_puzzle_anchor_pack_response(session.anchor_pack), + draft: session.draft.map(map_puzzle_result_draft_response), + messages: session + .messages + .into_iter() + .map(map_puzzle_agent_message_response) + .collect(), + last_assistant_reply: session.last_assistant_reply, + published_profile_id: session.published_profile_id, + suggested_actions: session + .suggested_actions + .into_iter() + .map(map_puzzle_suggested_action_response) + .collect(), + result_preview: session.result_preview.map(map_puzzle_result_preview_response), + updated_at: session.updated_at, + } +} + +fn map_puzzle_anchor_pack_response(anchor_pack: PuzzleAnchorPackRecord) -> PuzzleAnchorPackResponse { + PuzzleAnchorPackResponse { + theme_promise: map_puzzle_anchor_item_response(anchor_pack.theme_promise), + visual_subject: map_puzzle_anchor_item_response(anchor_pack.visual_subject), + visual_mood: map_puzzle_anchor_item_response(anchor_pack.visual_mood), + composition_hooks: map_puzzle_anchor_item_response(anchor_pack.composition_hooks), + tags_and_forbidden: map_puzzle_anchor_item_response(anchor_pack.tags_and_forbidden), + } +} + +fn map_puzzle_anchor_item_response(anchor: PuzzleAnchorItemRecord) -> PuzzleAnchorItemResponse { + PuzzleAnchorItemResponse { + key: anchor.key, + label: anchor.label, + value: anchor.value, + status: anchor.status, + } +} + +fn map_puzzle_result_draft_response(draft: PuzzleResultDraftRecord) -> PuzzleResultDraftResponse { + PuzzleResultDraftResponse { + level_name: draft.level_name, + summary: draft.summary, + theme_tags: draft.theme_tags, + forbidden_directives: draft.forbidden_directives, + creator_intent: draft.creator_intent.map(map_puzzle_creator_intent_response), + anchor_pack: map_puzzle_anchor_pack_response(draft.anchor_pack), + candidates: draft + .candidates + .into_iter() + .map(map_puzzle_generated_image_candidate_response) + .collect(), + selected_candidate_id: draft.selected_candidate_id, + cover_image_src: draft.cover_image_src, + cover_asset_id: draft.cover_asset_id, + generation_status: draft.generation_status, + } +} + +fn map_puzzle_creator_intent_response( + intent: PuzzleCreatorIntentRecord, +) -> PuzzleCreatorIntentResponse { + PuzzleCreatorIntentResponse { + source_mode: intent.source_mode, + raw_messages_summary: intent.raw_messages_summary, + theme_promise: intent.theme_promise, + visual_subject: intent.visual_subject, + visual_mood: intent.visual_mood, + composition_hooks: intent.composition_hooks, + theme_tags: intent.theme_tags, + forbidden_directives: intent.forbidden_directives, + } +} + +fn map_puzzle_generated_image_candidate_response( + candidate: PuzzleGeneratedImageCandidateRecord, +) -> PuzzleGeneratedImageCandidateResponse { + PuzzleGeneratedImageCandidateResponse { + candidate_id: candidate.candidate_id, + image_src: candidate.image_src, + asset_id: candidate.asset_id, + prompt: candidate.prompt, + actual_prompt: candidate.actual_prompt, + source_type: candidate.source_type, + selected: candidate.selected, + } +} + +fn map_puzzle_agent_message_response( + message: PuzzleAgentMessageRecord, +) -> PuzzleAgentMessageResponse { + PuzzleAgentMessageResponse { + id: message.message_id, + role: message.role, + kind: message.kind, + text: message.text, + created_at: message.created_at, + } +} + +fn map_puzzle_suggested_action_response( + action: PuzzleAgentSuggestedActionRecord, +) -> PuzzleAgentSuggestedActionResponse { + PuzzleAgentSuggestedActionResponse { + id: action.action_id, + action_type: action.action_type, + label: action.label, + } +} + +fn map_puzzle_result_preview_response( + preview: PuzzleResultPreviewRecord, +) -> PuzzleResultPreviewEnvelopeResponse { + PuzzleResultPreviewEnvelopeResponse { + draft: map_puzzle_result_draft_response(preview.draft), + blockers: preview + .blockers + .into_iter() + .map(map_puzzle_result_preview_blocker_response) + .collect(), + quality_findings: preview + .quality_findings + .into_iter() + .map(map_puzzle_result_preview_finding_response) + .collect(), + publish_ready: preview.publish_ready, + } +} + +fn map_puzzle_result_preview_blocker_response( + blocker: PuzzleResultPreviewBlockerRecord, +) -> PuzzleResultPreviewBlockerResponse { + PuzzleResultPreviewBlockerResponse { + id: blocker.blocker_id, + code: blocker.code, + message: blocker.message, + } +} + +fn map_puzzle_result_preview_finding_response( + finding: PuzzleResultPreviewFindingRecord, +) -> PuzzleResultPreviewFindingResponse { + PuzzleResultPreviewFindingResponse { + id: finding.finding_id, + severity: finding.severity, + code: finding.code, + message: finding.message, + } +} + +fn map_puzzle_work_summary_response(item: PuzzleWorkProfileRecord) -> PuzzleWorkSummaryResponse { + PuzzleWorkSummaryResponse { + work_id: item.work_id, + profile_id: item.profile_id, + owner_user_id: item.owner_user_id, + source_session_id: item.source_session_id, + author_display_name: item.author_display_name, + level_name: item.level_name, + summary: item.summary, + theme_tags: item.theme_tags, + cover_image_src: item.cover_image_src, + cover_asset_id: item.cover_asset_id, + publication_status: item.publication_status, + updated_at: item.updated_at, + published_at: item.published_at, + play_count: item.play_count, + publish_ready: item.publish_ready, + } +} + +fn map_puzzle_work_profile_response(item: PuzzleWorkProfileRecord) -> PuzzleWorkProfileResponse { + PuzzleWorkProfileResponse { + summary: map_puzzle_work_summary_response(item.clone()), + anchor_pack: map_puzzle_anchor_pack_response(item.anchor_pack), + } +} + +fn map_puzzle_run_response(run: PuzzleRunRecord) -> PuzzleRunSnapshotResponse { + PuzzleRunSnapshotResponse { + run_id: run.run_id, + entry_profile_id: run.entry_profile_id, + cleared_level_count: run.cleared_level_count, + current_level_index: run.current_level_index, + current_grid_size: run.current_grid_size, + played_profile_ids: run.played_profile_ids, + previous_level_tags: run.previous_level_tags, + current_level: run.current_level.map(map_puzzle_runtime_level_response), + recommended_next_profile_id: run.recommended_next_profile_id, + } +} + +fn map_puzzle_runtime_level_response( + level: spacetime_client::PuzzleRuntimeLevelRecord, +) -> PuzzleRuntimeLevelSnapshotResponse { + PuzzleRuntimeLevelSnapshotResponse { + run_id: level.run_id, + level_index: level.level_index, + grid_size: level.grid_size, + profile_id: level.profile_id, + level_name: level.level_name, + author_display_name: level.author_display_name, + theme_tags: level.theme_tags, + cover_image_src: level.cover_image_src, + board: map_puzzle_board_response(level.board), + status: level.status, + } +} + +fn map_puzzle_board_response( + board: spacetime_client::PuzzleBoardRecord, +) -> PuzzleBoardSnapshotResponse { + PuzzleBoardSnapshotResponse { + rows: board.rows, + cols: board.cols, + pieces: board + .pieces + .into_iter() + .map(|piece| PuzzlePieceStateResponse { + piece_id: piece.piece_id, + correct_row: piece.correct_row, + correct_col: piece.correct_col, + current_row: piece.current_row, + current_col: piece.current_col, + merged_group_id: piece.merged_group_id, + }) + .collect(), + merged_groups: board + .merged_groups + .into_iter() + .map(|group| PuzzleMergedGroupStateResponse { + group_id: group.group_id, + piece_ids: group.piece_ids, + occupied_cells: group + .occupied_cells + .into_iter() + .map(|cell| PuzzleCellPositionResponse { + row: cell.row, + col: cell.col, + }) + .collect(), + }) + .collect(), + selected_piece_id: board.selected_piece_id, + all_tiles_resolved: board.all_tiles_resolved, + } +} + +fn resolve_author_display_name( + state: &AppState, + authenticated: &AuthenticatedAccessToken, +) -> String { + state + .auth_user_service() + .get_user_by_id(authenticated.claims().user_id()) + .ok() + .flatten() + .map(|user| user.display_name) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "玩家".to_string()) +} + +fn build_puzzle_welcome_text(seed_text: &str) -> String { + if seed_text.trim().is_empty() { + return "先告诉我你想做一张什么样的拼图图像,我会帮你收束成可发布的题材锚点。" + .to_string(); + } + + "我先接住你的画面灵感,再一起把它收束成正式拼图关卡。".to_string() +} + +fn ensure_non_empty( + request_context: &RequestContext, + provider: &str, + value: &str, + field_name: &str, +) -> Result<(), Response> { + if value.trim().is_empty() { + return Err(puzzle_error_response( + request_context, + provider, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": provider, + "message": format!("{field_name} is required"), + })), + )); + } + Ok(()) +} + +fn puzzle_bad_request( + request_context: &RequestContext, + provider: &str, + message: &str, +) -> Response { + puzzle_error_response( + request_context, + provider, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": provider, + "message": message, + })), + ) +} + +fn map_puzzle_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 + } + _ => StatusCode::BAD_GATEWAY, + }; + + AppError::from_status(status).with_details(json!({ + "provider": "spacetimedb", + "message": error.to_string(), + })) +} + +fn puzzle_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("puzzle")), + ); + response +} + +fn append_sse_event( + request_context: &RequestContext, + body: &mut String, + event_name: &str, + payload: &Value, +) -> Result<(), Response> { + let payload = serde_json::to_string(payload).map_err(|error| { + puzzle_error_response( + request_context, + PUZZLE_AGENT_API_BASE_PROVIDER, + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": PUZZLE_AGENT_API_BASE_PROVIDER, + "message": format!("SSE payload 序列化失败:{error}"), + })), + ) + })?; + body.push_str("event: "); + body.push_str(event_name); + body.push('\n'); + body.push_str("data: "); + body.push_str(&payload); + body.push_str("\n\n"); + Ok(()) +} + +fn build_event_stream_response(body: String) -> Response { + ( + [ + (header::CONTENT_TYPE, "text/event-stream; charset=utf-8"), + (header::CACHE_CONTROL, "no-cache, no-transform"), + (header::CONNECTION, "keep-alive"), + ], + body, + ) + .into_response() +} + +fn build_placeholder_puzzle_candidates( + session_id: &str, + level_name: &str, + prompt: &str, + candidate_count: u32, +) -> Result, String> { + let count = candidate_count.clamp(1, 2); + let mut items = Vec::with_capacity(count as usize); + + for index in 0..count { + let asset = save_placeholder_puzzle_asset( + session_id, + level_name, + &format!("candidate-{}", index + 1), + "cover", + "1536*1536", + Some(prompt), + ) + .map_err(|error| error.message().to_string())?; + items.push(PuzzleGeneratedImageCandidateResponse { + candidate_id: format!("{session_id}-candidate-{}", index + 1), + image_src: asset.image_src, + asset_id: asset.asset_id, + prompt: prompt.to_string(), + actual_prompt: Some(prompt.to_string()), + source_type: "generated".to_string(), + selected: index == 0, + }); + } + + Ok(items + .into_iter() + .map(|candidate| PuzzleGeneratedImageCandidateRecord { + candidate_id: candidate.candidate_id, + image_src: candidate.image_src, + asset_id: candidate.asset_id, + prompt: candidate.prompt, + actual_prompt: candidate.actual_prompt, + source_type: candidate.source_type, + selected: candidate.selected, + }) + .collect()) +} + +struct GeneratedPuzzleAssetResponse { + image_src: String, + asset_id: String, +} + +fn save_placeholder_puzzle_asset( + session_segment_seed: &str, + work_segment_seed: &str, + leaf_segment_seed: &str, + file_stem: &str, + size: &str, + prompt: Option<&str>, +) -> Result { + let asset_id = format!("{file_stem}-{}", current_utc_millis()); + let relative_dir = PathBuf::from("generated-puzzle-covers") + .join(sanitize_path_segment(session_segment_seed, "session")) + .join(sanitize_path_segment(work_segment_seed, "puzzle")) + .join(sanitize_path_segment(leaf_segment_seed, "candidate")) + .join(&asset_id); + let output_dir = resolve_public_output_dir(&relative_dir)?; + fs::create_dir_all(&output_dir).map_err(io_error)?; + let file_name = format!("{file_stem}.svg"); + let svg = build_puzzle_placeholder_svg(size, prompt.unwrap_or(file_stem)); + fs::write(output_dir.join(&file_name), svg).map_err(io_error)?; + + Ok(GeneratedPuzzleAssetResponse { + image_src: format!("/{}/{}", relative_dir.to_string_lossy().replace('\\', "/"), file_name), + asset_id, + }) +} + +fn build_puzzle_placeholder_svg(size: &str, label: &str) -> String { + let (width, height) = parse_size(size); + format!( + r##" + + + + + + + + + + + +{title} +Puzzle placeholder +"##, + width = width, + height = height, + cx1 = width / 4, + cy1 = height / 3, + r1 = (width.min(height) / 5).max(42), + cx2 = width * 3 / 4, + cy2 = height / 4, + r2 = (width.min(height) / 7).max(30), + frame_x = width / 9, + frame_y = height / 9, + frame_w = width * 7 / 9, + frame_h = height * 7 / 9, + frame_r = (width.min(height) / 20).max(18), + font_main = (width.min(height) / 14).max(22), + font_sub = (width.min(height) / 30).max(12), + title = escape_svg_text(label), + ) +} + +fn parse_size(size: &str) -> (u32, u32) { + let mut parts = size.split('*'); + let width = parts + .next() + .and_then(|value| value.trim().parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(1536); + let height = parts + .next() + .and_then(|value| value.trim().parse::().ok()) + .filter(|value| *value > 0) + .unwrap_or(1536); + (width, height) +} + +fn escape_svg_text(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + +fn sanitize_path_segment(value: &str, fallback: &str) -> String { + let sanitized = value + .trim() + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ('\u{4e00}'..='\u{9fff}').contains(&ch) { + ch + } else { + '-' + } + }) + .collect::() + .trim_matches('-') + .to_string(); + if sanitized.is_empty() { + fallback.to_string() + } else { + sanitized + } +} + +fn resolve_public_output_dir(relative_dir: &Path) -> Result { + let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(3) + .ok_or_else(|| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) + .with_message("无法定位仓库根目录") + })?; + Ok(workspace_root.join("public").join(relative_dir)) +} + +fn io_error(error: std::io::Error) -> AppError { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string()) +} + +fn current_utc_micros() -> i64 { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + (duration.as_secs() as i64) * 1_000_000 + i64::from(duration.subsec_micros()) +} + +fn current_utc_millis() -> u128 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() +} diff --git a/server-rs/crates/api-server/src/runtime_story.rs b/server-rs/crates/api-server/src/runtime_story.rs index ab48af08..b7af0dbf 100644 --- a/server-rs/crates/api-server/src/runtime_story.rs +++ b/server-rs/crates/api-server/src/runtime_story.rs @@ -1,6 +1,6 @@ use axum::{ Json, - extract::{Extension, State}, + extract::{Extension, Path, State, rejection::JsonRejection}, http::StatusCode, response::Response, }; @@ -54,6 +54,91 @@ pub async fn resolve_runtime_story_state( )) } +pub async fn get_runtime_story_state( + State(_state): State, + Path(session_id): Path, + Extension(request_context): Extension, + Extension(_authenticated): Extension, +) -> Result, Response> { + let session_id = normalize_required_string(session_id.as_str()).ok_or_else(|| { + runtime_story_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "runtime-story", + "field": "sessionId", + "message": "sessionId 不能为空", + })), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + build_runtime_story_state_response(&session_id, None, build_runtime_story_empty_snapshot(&session_id)), + )) +} + +pub async fn resolve_runtime_story_action( + State(_state): State, + Extension(request_context): Extension, + Extension(_authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let payload = optional_runtime_story_payload(payload)?; + let session_id = read_payload_session_id(&payload).ok_or_else(|| { + runtime_story_error_response( + &request_context, + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "runtime-story", + "field": "sessionId", + "message": "sessionId 不能为空", + })), + ) + })?; + let client_version = read_u32_field(&payload, "clientVersion"); + let snapshot = read_payload_snapshot(&payload) + .unwrap_or_else(|| build_runtime_story_empty_snapshot(&session_id)); + let mut response = build_runtime_story_state_response(&session_id, client_version, snapshot); + response.presentation.action_text = read_runtime_story_action_text(&payload).unwrap_or_default(); + + Ok(json_success_body(Some(&request_context), response)) +} + +pub async fn generate_runtime_story_initial( + State(_state): State, + Extension(request_context): Extension, + Extension(_authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let payload = optional_runtime_story_payload(payload)?; + let session_id = read_payload_session_id(&payload).unwrap_or_else(|| "runtime-main".to_string()); + let client_version = read_u32_field(&payload, "clientVersion"); + let snapshot = read_payload_snapshot(&payload) + .unwrap_or_else(|| build_runtime_story_empty_snapshot(&session_id)); + + Ok(json_success_body( + Some(&request_context), + build_runtime_story_state_response(&session_id, client_version, snapshot), + )) +} + +pub async fn generate_runtime_story_continue( + State(_state): State, + Extension(request_context): Extension, + Extension(_authenticated): Extension, + payload: Result, JsonRejection>, +) -> Result, Response> { + let payload = optional_runtime_story_payload(payload)?; + let session_id = read_payload_session_id(&payload).unwrap_or_else(|| "runtime-main".to_string()); + let client_version = read_u32_field(&payload, "clientVersion"); + let snapshot = read_payload_snapshot(&payload) + .unwrap_or_else(|| build_runtime_story_empty_snapshot(&session_id)); + + Ok(json_success_body( + Some(&request_context), + build_runtime_story_state_response(&session_id, client_version, snapshot), + )) +} + fn build_runtime_story_state_response( requested_session_id: &str, client_version: Option, @@ -107,6 +192,61 @@ fn build_runtime_story_state_response( } } +fn optional_runtime_story_payload( + payload: Result, JsonRejection>, +) -> Result { + match payload { + Ok(Json(value)) => Ok(value), + Err(error) if error.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE => Ok(json!({})), + Err(error) if error.status() == StatusCode::BAD_REQUEST => Ok(json!({})), + Err(error) => Err(AppError::from_status(StatusCode::BAD_REQUEST) + .with_details(json!({ + "provider": "runtime-story", + "message": error.body_text(), + })) + .into_response_with_context(None)), + } +} + +fn read_payload_session_id(payload: &Value) -> Option { + read_required_string_field(payload, "sessionId") + .or_else(|| read_field(payload, "action").and_then(|action| read_required_string_field(action, "sessionId"))) + .or_else(|| read_field(payload, "snapshot").and_then(|snapshot| { + read_object_field(snapshot, "gameState").and_then(read_runtime_session_id) + })) +} + +fn read_payload_snapshot(payload: &Value) -> Option { + let snapshot = read_field(payload, "snapshot")?.clone(); + serde_json::from_value(snapshot).ok() +} + +fn read_runtime_story_action_text(payload: &Value) -> Option { + let action = read_field(payload, "action")?; + read_optional_string_field(action, "functionId") + .or_else(|| read_optional_string_field(action, "type")) +} + +fn build_runtime_story_empty_snapshot(session_id: &str) -> RuntimeStorySnapshotPayload { + RuntimeStorySnapshotPayload { + saved_at: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()), + bottom_tab: "adventure".to_string(), + game_state: json!({ + "runtimeSessionId": session_id, + "runtimeActionVersion": 0, + "playerHp": 1, + "playerMaxHp": 1, + "playerMana": 0, + "playerMaxMana": 1, + "inBattle": false, + "npcInteractionActive": false + }), + current_story: None, + } +} + fn build_runtime_story_companions(game_state: &Value) -> Vec { read_array_field(game_state, "companions") .into_iter() diff --git a/server-rs/crates/module-big-fish/Cargo.toml b/server-rs/crates/module-big-fish/Cargo.toml new file mode 100644 index 00000000..f79978b1 --- /dev/null +++ b/server-rs/crates/module-big-fish/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "module-big-fish" +edition.workspace = true +version.workspace = true +license.workspace = true + +[features] +default = [] +spacetime-types = ["dep:spacetimedb"] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +shared-kernel = { path = "../shared-kernel" } +spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-big-fish/src/lib.rs b/server-rs/crates/module-big-fish/src/lib.rs new file mode 100644 index 00000000..42c3562d --- /dev/null +++ b/server-rs/crates/module-big-fish/src/lib.rs @@ -0,0 +1,1385 @@ +use std::{error::Error, fmt}; + +use serde::{Deserialize, Serialize}; +use shared_kernel::normalize_required_string; +#[cfg(feature = "spacetime-types")] +use spacetimedb::SpacetimeType; + +pub const BIG_FISH_SESSION_ID_PREFIX: &str = "big-fish-session-"; +pub const BIG_FISH_MESSAGE_ID_PREFIX: &str = "big-fish-message-"; +pub const BIG_FISH_OPERATION_ID_PREFIX: &str = "big-fish-operation-"; +pub const BIG_FISH_ASSET_SLOT_ID_PREFIX: &str = "big-fish-asset-"; +pub const BIG_FISH_RUN_ID_PREFIX: &str = "big-fish-run-"; +pub const BIG_FISH_DEFAULT_LEVEL_COUNT: u32 = 8; +pub const BIG_FISH_MIN_LEVEL_COUNT: u32 = 6; +pub const BIG_FISH_MAX_LEVEL_COUNT: u32 = 12; +pub const BIG_FISH_MERGE_COUNT_PER_UPGRADE: u32 = 3; +pub const BIG_FISH_OFFSCREEN_CULL_SECONDS: f32 = 3.0; +pub const BIG_FISH_TARGET_WILD_COUNT: usize = 12; +pub const BIG_FISH_VIEW_WIDTH: f32 = 720.0; +pub const BIG_FISH_VIEW_HEIGHT: f32 = 1280.0; +pub const BIG_FISH_WORLD_HALF_WIDTH: f32 = 900.0; +pub const BIG_FISH_WORLD_HALF_HEIGHT: f32 = 1600.0; + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishCreationStage { + CollectingAnchors, + DraftReady, + AssetRefining, + ReadyToPublish, + Published, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishAnchorStatus { + Confirmed, + Inferred, + Missing, + Locked, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishAgentMessageRole { + User, + Assistant, + System, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishAgentMessageKind { + Chat, + Summary, + ActionResult, + Warning, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishAssetKind { + LevelMainImage, + LevelMotion, + StageBackground, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishAssetStatus { + Missing, + Ready, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum BigFishRunStatus { + Running, + Won, + Failed, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAnchorItem { + pub key: String, + pub label: String, + pub value: String, + pub status: BigFishAnchorStatus, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAnchorPack { + pub gameplay_promise: BigFishAnchorItem, + pub ecology_visual_theme: BigFishAnchorItem, + pub growth_ladder: BigFishAnchorItem, + pub risk_tempo: BigFishAnchorItem, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishLevelBlueprint { + pub level: u32, + pub name: String, + pub one_line_fantasy: String, + pub silhouette_direction: String, + pub size_ratio: f32, + pub visual_prompt_seed: String, + pub motion_prompt_seed: String, + pub merge_source_level: Option, + pub prey_window: Vec, + pub threat_window: Vec, + pub is_final_level: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishBackgroundBlueprint { + pub theme: String, + pub color_mood: String, + pub foreground_hints: String, + pub midground_composition: String, + pub background_depth: String, + pub safe_play_area_hint: String, + pub spawn_edge_hint: String, + pub background_prompt_seed: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishRuntimeParams { + pub level_count: u32, + pub merge_count_per_upgrade: u32, + pub spawn_target_count: u32, + pub leader_move_speed: f32, + pub follower_catch_up_speed: f32, + pub offscreen_cull_seconds: f32, + pub prey_spawn_delta_levels: Vec, + pub threat_spawn_delta_levels: Vec, + pub win_level: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishGameDraft { + pub title: String, + pub subtitle: String, + pub core_fun: String, + pub ecology_theme: String, + pub levels: Vec, + pub background: BigFishBackgroundBlueprint, + pub runtime_params: BigFishRuntimeParams, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: BigFishAgentMessageRole, + pub kind: BigFishAgentMessageKind, + pub text: String, + pub created_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAssetSlotSnapshot { + pub slot_id: String, + pub session_id: String, + pub asset_kind: BigFishAssetKind, + pub level: Option, + pub motion_key: Option, + pub status: BigFishAssetStatus, + pub asset_url: Option, + pub prompt_snapshot: String, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAssetCoverage { + pub level_main_image_ready_count: u32, + pub level_motion_ready_count: u32, + pub background_ready: bool, + pub required_level_count: u32, + pub publish_ready: bool, + pub blockers: Vec, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: BigFishCreationStage, + pub anchor_pack: BigFishAnchorPack, + pub draft: Option, + pub asset_slots: Vec, + pub asset_coverage: BigFishAssetCoverage, + pub messages: Vec, + pub last_assistant_reply: Option, + pub publish_ready: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishVector2 { + pub x: f32, + pub y: f32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishRuntimeEntity { + pub entity_id: String, + pub level: u32, + pub position: BigFishVector2, + pub radius: f32, + pub offscreen_seconds: f32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishRuntimeSnapshot { + pub run_id: String, + pub session_id: String, + pub status: BigFishRunStatus, + pub tick: u64, + pub player_level: u32, + pub win_level: u32, + pub leader_entity_id: Option, + pub owned_entities: Vec, + pub wild_entities: Vec, + pub camera_center: BigFishVector2, + pub last_input: BigFishVector2, + pub event_log: Vec, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishSessionProcedureResult { + pub ok: bool, + pub session: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishRunProcedureResult { + pub ok: bool, + pub run: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishMessageSubmitInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub assistant_message_id: String, + pub submitted_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub compiled_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishAssetGenerateInput { + pub session_id: String, + pub owner_user_id: String, + pub asset_kind: BigFishAssetKind, + pub level: Option, + pub motion_key: Option, + pub generated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishPublishInput { + pub session_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishRunStartInput { + pub run_id: String, + pub session_id: String, + pub owner_user_id: String, + pub started_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BigFishRunInputSubmitInput { + pub run_id: String, + pub owner_user_id: String, + pub input_x: f32, + pub input_y: f32, + pub submitted_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BigFishRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BigFishFieldError { + MissingSessionId, + MissingOwnerUserId, + MissingMessageId, + MissingMessageText, + MissingRunId, + MissingDraft, + InvalidLevel, + InvalidAssetKind, + InvalidRunState, +} + +impl BigFishCreationStage { + pub fn as_str(self) -> &'static str { + match self { + Self::CollectingAnchors => "collecting_anchors", + Self::DraftReady => "draft_ready", + Self::AssetRefining => "asset_refining", + Self::ReadyToPublish => "ready_to_publish", + Self::Published => "published", + } + } +} + +impl BigFishAnchorStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Confirmed => "confirmed", + Self::Inferred => "inferred", + Self::Missing => "missing", + Self::Locked => "locked", + } + } +} + +impl BigFishAgentMessageRole { + pub fn as_str(self) -> &'static str { + match self { + Self::User => "user", + Self::Assistant => "assistant", + Self::System => "system", + } + } +} + +impl BigFishAgentMessageKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Chat => "chat", + Self::Summary => "summary", + Self::ActionResult => "action_result", + Self::Warning => "warning", + } + } +} + +impl BigFishAssetKind { + pub fn as_str(self) -> &'static str { + match self { + Self::LevelMainImage => "level_main_image", + Self::LevelMotion => "level_motion", + Self::StageBackground => "stage_background", + } + } +} + +impl BigFishAssetStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Missing => "missing", + Self::Ready => "ready", + } + } +} + +impl BigFishRunStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Running => "running", + Self::Won => "won", + Self::Failed => "failed", + } + } +} + +pub fn empty_anchor_pack() -> BigFishAnchorPack { + BigFishAnchorPack { + gameplay_promise: BigFishAnchorItem { + key: "gameplayPromise".to_string(), + label: "玩法承诺".to_string(), + value: String::new(), + status: BigFishAnchorStatus::Missing, + }, + ecology_visual_theme: BigFishAnchorItem { + key: "ecologyVisualTheme".to_string(), + label: "生态与视觉母题".to_string(), + value: String::new(), + status: BigFishAnchorStatus::Missing, + }, + growth_ladder: BigFishAnchorItem { + key: "growthLadder".to_string(), + label: "成长阶梯".to_string(), + value: String::new(), + status: BigFishAnchorStatus::Missing, + }, + risk_tempo: BigFishAnchorItem { + key: "riskTempo".to_string(), + label: "风险节奏".to_string(), + value: "平衡".to_string(), + status: BigFishAnchorStatus::Inferred, + }, + } +} + +pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> BigFishAnchorPack { + let source = normalize_required_string(latest_message.unwrap_or(seed_text)) + .or_else(|| normalize_required_string(seed_text)) + .unwrap_or_else(|| "深海弱小逆袭,逐级吞噬成长".to_string()); + let mut pack = empty_anchor_pack(); + pack.gameplay_promise.value = if source.contains("可爱") { + "可爱生态成长".to_string() + } else if source.contains("机械") { + "机械微生物吞并进化".to_string() + } else { + "弱小逆袭和群体吞并".to_string() + }; + pack.gameplay_promise.status = BigFishAnchorStatus::Inferred; + pack.ecology_visual_theme.value = if source.contains("机械") { + "机械微生物水域".to_string() + } else if source.contains("梦") { + "梦境纸鱼生态".to_string() + } else { + "深海生物生态".to_string() + }; + pack.ecology_visual_theme.status = BigFishAnchorStatus::Inferred; + pack.growth_ladder.value = "8 级连续进化,从幼小个体成长为终局巨兽".to_string(); + pack.growth_ladder.status = BigFishAnchorStatus::Inferred; + pack.risk_tempo.value = if source.contains("爽") { + "偏爽快".to_string() + } else if source.contains("压迫") { + "偏压迫".to_string() + } else { + "平衡".to_string() + }; + pack +} + +pub fn compile_default_draft(anchor_pack: &BigFishAnchorPack) -> BigFishGameDraft { + let level_count = BIG_FISH_DEFAULT_LEVEL_COUNT; + let theme = fallback_anchor_value(&anchor_pack.ecology_visual_theme, "深海生物生态"); + let core_fun = fallback_anchor_value(&anchor_pack.gameplay_promise, "弱小逆袭和群体吞并"); + let risk_tempo = fallback_anchor_value(&anchor_pack.risk_tempo, "平衡"); + + let levels = (1..=level_count) + .map(|level| build_level_blueprint(level, level_count, &theme)) + .collect(); + + BigFishGameDraft { + title: format!("{theme} 大鱼吃小鱼"), + subtitle: format!("{core_fun} · {risk_tempo}节奏"), + core_fun, + ecology_theme: theme.clone(), + levels, + background: BigFishBackgroundBlueprint { + theme: theme.clone(), + color_mood: "深蓝、青绿、带少量暖色生物光".to_string(), + foreground_hints: "轻微漂浮颗粒和边缘水草,不遮挡中央操作区".to_string(), + midground_composition: "中央留出清晰活动区域,边缘有出生缓冲层".to_string(), + background_depth: "纵深水域与远处体型剪影".to_string(), + safe_play_area_hint: "9:16 竖屏中央 70% 为主要活动区".to_string(), + spawn_edge_hint: "四周边缘作为野生实体出生区".to_string(), + background_prompt_seed: format!("{theme},竖屏 9:16,全屏游戏背景,无文字,无 UI 框"), + }, + runtime_params: BigFishRuntimeParams { + level_count, + merge_count_per_upgrade: BIG_FISH_MERGE_COUNT_PER_UPGRADE, + spawn_target_count: BIG_FISH_TARGET_WILD_COUNT as u32, + leader_move_speed: 160.0, + follower_catch_up_speed: 120.0, + offscreen_cull_seconds: BIG_FISH_OFFSCREEN_CULL_SECONDS, + prey_spawn_delta_levels: vec![1, 2], + threat_spawn_delta_levels: vec![1, 2], + win_level: level_count, + }, + } +} + +pub fn build_asset_coverage( + draft: Option<&BigFishGameDraft>, + asset_slots: &[BigFishAssetSlotSnapshot], +) -> BigFishAssetCoverage { + let required_level_count = draft + .map(|value| value.runtime_params.level_count) + .unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT); + let main_ready = asset_slots + .iter() + .filter(|slot| { + slot.asset_kind == BigFishAssetKind::LevelMainImage + && slot.status == BigFishAssetStatus::Ready + }) + .count() as u32; + let motion_ready = asset_slots + .iter() + .filter(|slot| { + slot.asset_kind == BigFishAssetKind::LevelMotion + && slot.status == BigFishAssetStatus::Ready + }) + .count() as u32; + let background_ready = asset_slots.iter().any(|slot| { + slot.asset_kind == BigFishAssetKind::StageBackground + && slot.status == BigFishAssetStatus::Ready + }); + + let required_motion_count = required_level_count * 2; + let mut blockers = Vec::new(); + if draft.is_none() { + blockers.push("玩法草稿尚未编译".to_string()); + } + if main_ready < required_level_count { + blockers.push(format!( + "还缺少 {} 个等级主图", + required_level_count.saturating_sub(main_ready) + )); + } + if motion_ready < required_motion_count { + blockers.push(format!( + "还缺少 {} 个基础动作", + required_motion_count.saturating_sub(motion_ready) + )); + } + if !background_ready { + blockers.push("还缺少活动区域背景图".to_string()); + } + + BigFishAssetCoverage { + level_main_image_ready_count: main_ready, + level_motion_ready_count: motion_ready, + background_ready, + required_level_count, + publish_ready: blockers.is_empty(), + blockers, + } +} + +pub fn build_generated_asset_slot( + session_id: &str, + draft: &BigFishGameDraft, + asset_kind: BigFishAssetKind, + level: Option, + motion_key: Option, + updated_at_micros: i64, +) -> Result { + let session_id = + normalize_required_string(session_id).ok_or(BigFishFieldError::MissingSessionId)?; + let prompt_snapshot = build_asset_prompt_snapshot(draft, asset_kind, level, motion_key.as_deref())?; + let slot_id = build_asset_slot_id(&session_id, asset_kind, level, motion_key.as_deref()); + + Ok(BigFishAssetSlotSnapshot { + slot_id, + session_id, + asset_kind, + level, + motion_key, + status: BigFishAssetStatus::Ready, + asset_url: Some(build_placeholder_asset_url(asset_kind, level, updated_at_micros)), + prompt_snapshot, + updated_at_micros, + }) +} + +pub fn build_initial_runtime_snapshot( + run_id: String, + session_id: String, + draft: &BigFishGameDraft, + now_micros: i64, +) -> BigFishRuntimeSnapshot { + let mut snapshot = BigFishRuntimeSnapshot { + run_id, + session_id, + status: BigFishRunStatus::Running, + tick: 0, + player_level: 1, + win_level: draft.runtime_params.win_level, + leader_entity_id: Some("owned-1".to_string()), + owned_entities: vec![BigFishRuntimeEntity { + entity_id: "owned-1".to_string(), + level: 1, + position: BigFishVector2 { x: 0.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 0.0, + }], + wild_entities: vec![ + BigFishRuntimeEntity { + entity_id: "wild-open-1".to_string(), + level: 1, + position: BigFishVector2 { x: 72.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 0.0, + }, + BigFishRuntimeEntity { + entity_id: "wild-open-2".to_string(), + level: 1, + position: BigFishVector2 { x: -88.0, y: 30.0 }, + radius: entity_radius(1), + offscreen_seconds: 0.0, + }, + ], + camera_center: BigFishVector2 { x: 0.0, y: 0.0 }, + last_input: BigFishVector2 { x: 0.0, y: 0.0 }, + event_log: vec!["开局生成 2 个同级可收编目标".to_string()], + updated_at_micros: now_micros, + }; + maintain_wild_pool(&mut snapshot, &draft.runtime_params); + snapshot +} + +pub fn advance_runtime_snapshot( + mut snapshot: BigFishRuntimeSnapshot, + params: &BigFishRuntimeParams, + input_x: f32, + input_y: f32, + now_micros: i64, +) -> BigFishRuntimeSnapshot { + if snapshot.status != BigFishRunStatus::Running { + return snapshot; + } + + let step_seconds = resolve_step_seconds(&snapshot, now_micros); + snapshot.tick = snapshot.tick.saturating_add(1); + snapshot.last_input = normalize_input(input_x, input_y); + move_owned_entities(&mut snapshot, params, step_seconds); + resolve_collisions(&mut snapshot, params); + apply_chain_merges(&mut snapshot, params); + refresh_player_leader(&mut snapshot); + apply_win_or_fail(&mut snapshot, params); + update_wild_culling(&mut snapshot, params, step_seconds); + maintain_wild_pool(&mut snapshot, params); + snapshot.updated_at_micros = now_micros; + snapshot +} + +pub fn validate_session_get_input(input: &BigFishSessionGetInput) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id) +} + +pub fn validate_session_create_input( + input: &BigFishSessionCreateInput, +) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id)?; + if normalize_required_string(&input.welcome_message_id).is_none() { + return Err(BigFishFieldError::MissingMessageId); + } + Ok(()) +} + +pub fn validate_message_submit_input( + input: &BigFishMessageSubmitInput, +) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id)?; + if normalize_required_string(&input.user_message_id).is_none() + || normalize_required_string(&input.assistant_message_id).is_none() + { + return Err(BigFishFieldError::MissingMessageId); + } + if normalize_required_string(&input.user_message_text).is_none() { + return Err(BigFishFieldError::MissingMessageText); + } + Ok(()) +} + +pub fn validate_draft_compile_input( + input: &BigFishDraftCompileInput, +) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id) +} + +pub fn validate_asset_generate_input( + input: &BigFishAssetGenerateInput, + draft: &BigFishGameDraft, +) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id)?; + match input.asset_kind { + BigFishAssetKind::LevelMainImage => validate_level(input.level, draft), + BigFishAssetKind::LevelMotion => { + validate_level(input.level, draft)?; + match input.motion_key.as_deref() { + Some("idle_float" | "move_swim") => Ok(()), + _ => Err(BigFishFieldError::InvalidAssetKind), + } + } + BigFishAssetKind::StageBackground => Ok(()), + } +} + +pub fn validate_publish_input(input: &BigFishPublishInput) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id) +} + +pub fn validate_run_start_input(input: &BigFishRunStartInput) -> Result<(), BigFishFieldError> { + validate_session_owner(&input.session_id, &input.owner_user_id)?; + if normalize_required_string(&input.run_id).is_none() { + return Err(BigFishFieldError::MissingRunId); + } + Ok(()) +} + +pub fn validate_run_get_input(input: &BigFishRunGetInput) -> Result<(), BigFishFieldError> { + if normalize_required_string(&input.run_id).is_none() { + return Err(BigFishFieldError::MissingRunId); + } + if normalize_required_string(&input.owner_user_id).is_none() { + return Err(BigFishFieldError::MissingOwnerUserId); + } + Ok(()) +} + +pub fn validate_run_input_submit_input( + input: &BigFishRunInputSubmitInput, +) -> Result<(), BigFishFieldError> { + if normalize_required_string(&input.run_id).is_none() { + return Err(BigFishFieldError::MissingRunId); + } + if normalize_required_string(&input.owner_user_id).is_none() { + return Err(BigFishFieldError::MissingOwnerUserId); + } + Ok(()) +} + +pub fn serialize_anchor_pack(anchor_pack: &BigFishAnchorPack) -> Result { + serde_json::to_string(anchor_pack) +} + +pub fn deserialize_anchor_pack(value: &str) -> Result { + serde_json::from_str(value) +} + +pub fn serialize_draft(draft: &BigFishGameDraft) -> Result { + serde_json::to_string(draft) +} + +pub fn deserialize_draft(value: &str) -> Result { + serde_json::from_str(value) +} + +pub fn serialize_asset_coverage( + coverage: &BigFishAssetCoverage, +) -> Result { + serde_json::to_string(coverage) +} + +pub fn deserialize_asset_coverage(value: &str) -> Result { + serde_json::from_str(value) +} + +pub fn serialize_runtime_snapshot( + snapshot: &BigFishRuntimeSnapshot, +) -> Result { + serde_json::to_string(snapshot) +} + +pub fn deserialize_runtime_snapshot( + value: &str, +) -> Result { + serde_json::from_str(value) +} + +fn fallback_anchor_value(anchor: &BigFishAnchorItem, fallback: &str) -> String { + normalize_required_string(&anchor.value).unwrap_or_else(|| fallback.to_string()) +} + +fn build_level_blueprint(level: u32, level_count: u32, theme: &str) -> BigFishLevelBlueprint { + let prey_window = (1..level) + .rev() + .take(2) + .collect::>() + .into_iter() + .rev() + .collect(); + let threat_window = ((level + 1)..=(level + 2).min(level_count)).collect::>(); + BigFishLevelBlueprint { + level, + name: format!("{theme} L{level}"), + one_line_fantasy: if level == level_count { + "终局巨兽形态,获得即可通关".to_string() + } else { + format!("第 {level} 阶实体,继续吞噬同级和低级个体成长") + }, + silhouette_direction: format!("体型约为初始的 {:.1} 倍,轮廓更清晰", 1.0 + level as f32 * 0.22), + size_ratio: 1.0 + (level.saturating_sub(1) as f32 * 0.22), + visual_prompt_seed: format!("{theme} 第 {level} 级实体主图,透明背景,清晰轮廓"), + motion_prompt_seed: format!("{theme} 第 {level} 级实体 idle_float 与 move_swim 动作"), + merge_source_level: if level == 1 { None } else { Some(level - 1) }, + prey_window, + threat_window, + is_final_level: level == level_count, + } +} + +fn build_asset_prompt_snapshot( + draft: &BigFishGameDraft, + asset_kind: BigFishAssetKind, + level: Option, + motion_key: Option<&str>, +) -> Result { + match asset_kind { + BigFishAssetKind::LevelMainImage => { + let level = level.ok_or(BigFishFieldError::InvalidLevel)?; + let blueprint = draft + .levels + .iter() + .find(|item| item.level == level) + .ok_or(BigFishFieldError::InvalidLevel)?; + Ok(blueprint.visual_prompt_seed.clone()) + } + BigFishAssetKind::LevelMotion => { + let level = level.ok_or(BigFishFieldError::InvalidLevel)?; + let blueprint = draft + .levels + .iter() + .find(|item| item.level == level) + .ok_or(BigFishFieldError::InvalidLevel)?; + let motion_key = motion_key.ok_or(BigFishFieldError::InvalidAssetKind)?; + Ok(format!("{},动作位:{}", blueprint.motion_prompt_seed, motion_key)) + } + BigFishAssetKind::StageBackground => Ok(draft.background.background_prompt_seed.clone()), + } +} + +fn build_asset_slot_id( + session_id: &str, + asset_kind: BigFishAssetKind, + level: Option, + motion_key: Option<&str>, +) -> String { + let level_part = level + .map(|value| value.to_string()) + .unwrap_or_else(|| "stage".to_string()); + let motion_part = motion_key.unwrap_or("main"); + format!( + "{BIG_FISH_ASSET_SLOT_ID_PREFIX}{session_id}_{}_{}_{}", + asset_kind.as_str(), + level_part, + motion_part + ) +} + +fn build_placeholder_asset_url( + asset_kind: BigFishAssetKind, + level: Option, + seed_micros: i64, +) -> String { + let level_part = level + .map(|value| format!("level-{value}")) + .unwrap_or_else(|| "stage".to_string()); + format!( + "/generated-big-fish/{}/{}/{}.png", + asset_kind.as_str(), + level_part, + seed_micros + ) +} + +fn validate_session_owner(session_id: &str, owner_user_id: &str) -> Result<(), BigFishFieldError> { + if normalize_required_string(session_id).is_none() { + return Err(BigFishFieldError::MissingSessionId); + } + if normalize_required_string(owner_user_id).is_none() { + return Err(BigFishFieldError::MissingOwnerUserId); + } + Ok(()) +} + +fn validate_level(level: Option, draft: &BigFishGameDraft) -> Result<(), BigFishFieldError> { + match level { + Some(value) if (1..=draft.runtime_params.level_count).contains(&value) => Ok(()), + _ => Err(BigFishFieldError::InvalidLevel), + } +} + +fn normalize_input(x: f32, y: f32) -> BigFishVector2 { + let length = (x * x + y * y).sqrt(); + if length <= 1.0 { + return BigFishVector2 { x, y }; + } + BigFishVector2 { + x: x / length, + y: y / length, + } +} + +/// 运行态仍由 `POST input` 触发推进,因此“屏外 3 秒”这类规则必须按真实秒数累计, +/// 否则会随着输入频率变化而漂移。 +fn resolve_step_seconds(snapshot: &BigFishRuntimeSnapshot, now_micros: i64) -> f32 { + ((now_micros - snapshot.updated_at_micros).max(0) as f32) / 1_000_000.0 +} + +fn move_owned_entities( + snapshot: &mut BigFishRuntimeSnapshot, + params: &BigFishRuntimeParams, + step_seconds: f32, +) { + let input = snapshot.last_input.clone(); + if let Some(leader) = snapshot.owned_entities.first_mut() { + leader.position.x = clamp_world(leader.position.x + input.x * params.leader_move_speed * step_seconds, true); + leader.position.y = clamp_world(leader.position.y + input.y * params.leader_move_speed * step_seconds, false); + snapshot.camera_center = leader.position.clone(); + } + + let leader_position = snapshot.camera_center.clone(); + for (index, follower) in snapshot.owned_entities.iter_mut().enumerate().skip(1) { + let slot_offset = ((index as f32) * 0.7).sin() * 36.0; + let target = BigFishVector2 { + x: leader_position.x - 42.0 - index as f32 * 8.0, + y: leader_position.y + slot_offset, + }; + let delta_x = target.x - follower.position.x; + let delta_y = target.y - follower.position.y; + let distance = (delta_x * delta_x + delta_y * delta_y).sqrt(); + if distance <= f32::EPSILON { + continue; + } + let catch_up_ratio = + (params.follower_catch_up_speed * step_seconds / distance).clamp(0.0, 1.0); + follower.position.x += delta_x * catch_up_ratio; + follower.position.y += delta_y * catch_up_ratio; + } +} + +fn resolve_collisions(snapshot: &mut BigFishRuntimeSnapshot, _params: &BigFishRuntimeParams) { + let mut owned_to_remove = Vec::new(); + let mut wild_to_remove = Vec::new(); + let mut newly_owned = Vec::new(); + + for (owned_index, owned) in snapshot.owned_entities.iter().enumerate() { + for (wild_index, wild) in snapshot.wild_entities.iter().enumerate() { + if wild_to_remove.contains(&wild_index) || owned_to_remove.contains(&owned_index) { + continue; + } + if distance(&owned.position, &wild.position) > owned.radius + wild.radius { + continue; + } + + if owned.level >= wild.level { + wild_to_remove.push(wild_index); + newly_owned.push(BigFishRuntimeEntity { + entity_id: format!("owned-from-{}-{}", wild.entity_id, snapshot.tick), + level: wild.level, + position: wild.position.clone(), + radius: entity_radius(wild.level), + offscreen_seconds: 0.0, + }); + snapshot.event_log.push(format!("收编 {} 级实体", wild.level)); + } else { + owned_to_remove.push(owned_index); + snapshot.event_log.push(format!("{} 级己方实体被 {} 级野生实体吃掉", owned.level, wild.level)); + } + } + } + + remove_indices(&mut snapshot.wild_entities, &wild_to_remove); + remove_indices(&mut snapshot.owned_entities, &owned_to_remove); + snapshot.owned_entities.extend(newly_owned); +} + +fn apply_chain_merges(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) { + loop { + let mut merged = false; + for level in 1..params.win_level { + let indices = snapshot + .owned_entities + .iter() + .enumerate() + .filter_map(|(index, entity)| (entity.level == level).then_some(index)) + .take(params.merge_count_per_upgrade as usize) + .collect::>(); + if indices.len() < params.merge_count_per_upgrade as usize { + continue; + } + + let center = average_position(&indices, &snapshot.owned_entities); + remove_indices(&mut snapshot.owned_entities, &indices); + snapshot.owned_entities.push(BigFishRuntimeEntity { + entity_id: format!("owned-merge-{}-{}", level + 1, snapshot.tick), + level: level + 1, + position: center, + radius: entity_radius(level + 1), + offscreen_seconds: 0.0, + }); + snapshot.event_log.push(format!("3 个 {} 级实体合成 {} 级", level, level + 1)); + merged = true; + break; + } + + if !merged { + break; + } + } +} + +fn refresh_player_leader(snapshot: &mut BigFishRuntimeSnapshot) { + snapshot.owned_entities.sort_by(|left, right| { + right + .level + .cmp(&left.level) + .then_with(|| { + distance(&left.position, &snapshot.camera_center) + .partial_cmp(&distance(&right.position, &snapshot.camera_center)) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .then_with(|| left.entity_id.cmp(&right.entity_id)) + }); + snapshot.leader_entity_id = snapshot.owned_entities.first().map(|entity| entity.entity_id.clone()); + snapshot.player_level = snapshot + .owned_entities + .iter() + .map(|entity| entity.level) + .max() + .unwrap_or(0); + if let Some(leader) = snapshot.owned_entities.first() { + snapshot.camera_center = leader.position.clone(); + } +} + +fn apply_win_or_fail(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) { + if snapshot.owned_entities.is_empty() { + snapshot.status = BigFishRunStatus::Failed; + snapshot.event_log.push("己方实体归零,本局失败".to_string()); + return; + } + if snapshot.player_level >= params.win_level { + snapshot.status = BigFishRunStatus::Won; + snapshot.event_log.push("获得最高等级实体,通关".to_string()); + } +} + +fn update_wild_culling( + snapshot: &mut BigFishRuntimeSnapshot, + params: &BigFishRuntimeParams, + step_seconds: f32, +) { + let player_level = snapshot.player_level; + for wild in &mut snapshot.wild_entities { + let should_cull_level = wild.level == player_level + || wild.level >= player_level.saturating_add(3) + || wild.level.saturating_add(3) <= player_level; + if !should_cull_level { + wild.offscreen_seconds = 0.0; + continue; + } + + if is_offscreen(&wild.position, &snapshot.camera_center, wild.radius) { + wild.offscreen_seconds += step_seconds; + } else { + wild.offscreen_seconds = 0.0; + } + } + snapshot + .wild_entities + .retain(|wild| wild.offscreen_seconds < params.offscreen_cull_seconds); +} + +fn maintain_wild_pool(snapshot: &mut BigFishRuntimeSnapshot, params: &BigFishRuntimeParams) { + if snapshot.status != BigFishRunStatus::Running { + return; + } + let mut next_index = snapshot.wild_entities.len() + snapshot.tick as usize; + while snapshot.wild_entities.len() < params.spawn_target_count as usize { + let level = next_spawn_level(snapshot.player_level.max(1), params.win_level, next_index); + snapshot.wild_entities.push(BigFishRuntimeEntity { + entity_id: format!("wild-{}-{}", snapshot.tick, next_index), + level, + position: spawn_position(&snapshot.camera_center, next_index), + radius: entity_radius(level), + offscreen_seconds: 0.0, + }); + next_index += 1; + } +} + +fn next_spawn_level(player_level: u32, win_level: u32, index: usize) -> u32 { + if player_level == 1 && index % 4 < 2 { + return 1; + } + let deltas = [-2_i32, -1, 1, 2]; + let delta = deltas[index % deltas.len()]; + (player_level as i32 + delta).clamp(1, win_level as i32) as u32 +} + +fn spawn_position(center: &BigFishVector2, index: usize) -> BigFishVector2 { + let side = index % 4; + let offset = ((index as f32 * 37.0) % 420.0) - 210.0; + match side { + 0 => BigFishVector2 { + x: center.x - BIG_FISH_VIEW_WIDTH * 0.62, + y: center.y + offset, + }, + 1 => BigFishVector2 { + x: center.x + BIG_FISH_VIEW_WIDTH * 0.62, + y: center.y + offset, + }, + 2 => BigFishVector2 { + x: center.x + offset, + y: center.y - BIG_FISH_VIEW_HEIGHT * 0.58, + }, + _ => BigFishVector2 { + x: center.x + offset, + y: center.y + BIG_FISH_VIEW_HEIGHT * 0.58, + }, + } +} + +fn remove_indices(items: &mut Vec, indices: &[usize]) { + let mut sorted = indices.to_vec(); + sorted.sort_unstable(); + sorted.dedup(); + for index in sorted.into_iter().rev() { + if index < items.len() { + items.remove(index); + } + } +} + +fn average_position(indices: &[usize], entities: &[BigFishRuntimeEntity]) -> BigFishVector2 { + let mut x = 0.0; + let mut y = 0.0; + for index in indices { + x += entities[*index].position.x; + y += entities[*index].position.y; + } + let count = indices.len().max(1) as f32; + BigFishVector2 { + x: x / count, + y: y / count, + } +} + +fn distance(left: &BigFishVector2, right: &BigFishVector2) -> f32 { + let dx = left.x - right.x; + let dy = left.y - right.y; + (dx * dx + dy * dy).sqrt() +} + +fn is_offscreen(position: &BigFishVector2, camera: &BigFishVector2, radius: f32) -> bool { + let half_w = BIG_FISH_VIEW_WIDTH / 2.0; + let half_h = BIG_FISH_VIEW_HEIGHT / 2.0; + position.x + radius < camera.x - half_w + || position.x - radius > camera.x + half_w + || position.y + radius < camera.y - half_h + || position.y - radius > camera.y + half_h +} + +fn clamp_world(value: f32, horizontal: bool) -> f32 { + let limit = if horizontal { + BIG_FISH_WORLD_HALF_WIDTH + } else { + BIG_FISH_WORLD_HALF_HEIGHT + }; + value.clamp(-limit, limit) +} + +fn entity_radius(level: u32) -> f32 { + 18.0 + level as f32 * 4.0 +} + +impl fmt::Display for BigFishFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingSessionId => f.write_str("big_fish.session_id 不能为空"), + Self::MissingOwnerUserId => f.write_str("big_fish.owner_user_id 不能为空"), + Self::MissingMessageId => f.write_str("big_fish.message_id 不能为空"), + Self::MissingMessageText => f.write_str("big_fish.message_text 不能为空"), + Self::MissingRunId => f.write_str("big_fish.run_id 不能为空"), + Self::MissingDraft => f.write_str("big_fish.draft 尚未编译"), + Self::InvalidLevel => f.write_str("big_fish.level 不在合法等级范围内"), + Self::InvalidAssetKind => f.write_str("big_fish.asset_kind 或动作位非法"), + Self::InvalidRunState => f.write_str("big_fish.run 当前状态不允许推进"), + } + } +} + +impl Error for BigFishFieldError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_draft_compiles_eight_levels_with_fixed_runtime_params() { + let draft = compile_default_draft(&infer_anchor_pack("机械深海,节奏偏爽", None)); + + assert_eq!(draft.levels.len(), BIG_FISH_DEFAULT_LEVEL_COUNT as usize); + assert_eq!(draft.runtime_params.merge_count_per_upgrade, 3); + assert_eq!(draft.runtime_params.offscreen_cull_seconds, 3.0); + assert_eq!(draft.runtime_params.prey_spawn_delta_levels, vec![1, 2]); + assert_eq!(draft.runtime_params.threat_spawn_delta_levels, vec![1, 2]); + assert!(draft.levels.last().is_some_and(|level| level.is_final_level)); + } + + #[test] + fn asset_coverage_requires_main_images_two_motions_and_background() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let coverage = build_asset_coverage(Some(&draft), &[]); + + assert!(!coverage.publish_ready); + assert_eq!(coverage.required_level_count, 8); + assert!(coverage.blockers.iter().any(|item| item.contains("等级主图"))); + assert!(coverage.blockers.iter().any(|item| item.contains("基础动作"))); + assert!(coverage.blockers.iter().any(|item| item.contains("背景图"))); + } + + #[test] + fn same_level_wild_entity_can_be_collected_at_start() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let mut snapshot = build_initial_runtime_snapshot( + "run-1".to_string(), + "session-1".to_string(), + &draft, + 1, + ); + snapshot.wild_entities[0].position = BigFishVector2 { x: 1.0, y: 0.0 }; + + let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2); + + assert!(next.owned_entities.len() >= 2); + assert!( + next.event_log + .iter() + .any(|event| event.contains("收编 1 级实体")) + ); + } + + #[test] + fn three_owned_entities_merge_into_next_level() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let mut snapshot = build_initial_runtime_snapshot( + "run-merge".to_string(), + "session-merge".to_string(), + &draft, + 1, + ); + snapshot.wild_entities.clear(); + snapshot.owned_entities.push(BigFishRuntimeEntity { + entity_id: "owned-2".to_string(), + level: 1, + position: BigFishVector2 { x: 4.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 0.0, + }); + snapshot.owned_entities.push(BigFishRuntimeEntity { + entity_id: "owned-3".to_string(), + level: 1, + position: BigFishVector2 { x: 8.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 0.0, + }); + + let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2); + + assert!(next.owned_entities.iter().any(|entity| entity.level == 2)); + } + + #[test] + fn final_level_immediately_wins() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let mut snapshot = build_initial_runtime_snapshot( + "run-win".to_string(), + "session-win".to_string(), + &draft, + 1, + ); + snapshot.owned_entities[0].level = draft.runtime_params.win_level; + + let next = advance_runtime_snapshot(snapshot, &draft.runtime_params, 0.0, 0.0, 2); + + assert_eq!(next.status, BigFishRunStatus::Won); + } + + #[test] + fn offscreen_same_level_wild_entity_is_removed_after_three_seconds() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let mut snapshot = build_initial_runtime_snapshot( + "run-cull".to_string(), + "session-cull".to_string(), + &draft, + 1, + ); + snapshot.wild_entities.clear(); + snapshot.wild_entities.push(BigFishRuntimeEntity { + entity_id: "wild-cull".to_string(), + level: 1, + position: BigFishVector2 { x: 1000.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 2.8, + }); + snapshot.updated_at_micros = 1_000_000; + + let next = advance_runtime_snapshot( + snapshot, + &draft.runtime_params, + 0.0, + 0.0, + 1_250_000, + ); + + assert!(!next.wild_entities.iter().any(|entity| entity.entity_id == "wild-cull")); + } + + #[test] + fn offscreen_same_level_wild_entity_is_kept_before_three_seconds_elapsed() { + let draft = compile_default_draft(&infer_anchor_pack("深海", None)); + let mut snapshot = build_initial_runtime_snapshot( + "run-cull-safe".to_string(), + "session-cull-safe".to_string(), + &draft, + 1, + ); + snapshot.wild_entities.clear(); + snapshot.wild_entities.push(BigFishRuntimeEntity { + entity_id: "wild-cull-safe".to_string(), + level: 1, + position: BigFishVector2 { x: 1000.0, y: 0.0 }, + radius: entity_radius(1), + offscreen_seconds: 2.7, + }); + snapshot.updated_at_micros = 1_000_000; + + let next = advance_runtime_snapshot( + snapshot, + &draft.runtime_params, + 0.0, + 0.0, + 1_200_000, + ); + + assert!(next + .wild_entities + .iter() + .any(|entity| entity.entity_id == "wild-cull-safe")); + } +} diff --git a/server-rs/crates/module-custom-world/src/lib.rs b/server-rs/crates/module-custom-world/src/lib.rs index 2eae38c5..48814f77 100644 --- a/server-rs/crates/module-custom-world/src/lib.rs +++ b/server-rs/crates/module-custom-world/src/lib.rs @@ -153,6 +153,7 @@ pub enum CustomWorldFieldError { MissingMessageText, MissingOperationId, MissingPhaseLabel, + MissingAction, InvalidProgressPercent, MissingCardId, MissingCardTitle, @@ -290,6 +291,7 @@ pub struct CustomWorldAgentSessionSnapshot { pub lock_state_json: Option, pub draft_profile_json: Option, pub last_assistant_reply: Option, + pub publish_gate_json: Option, pub result_preview_json: Option, pub pending_clarifications_json: String, pub quality_findings_json: String, @@ -297,6 +299,7 @@ pub struct CustomWorldAgentSessionSnapshot { pub recommended_replies_json: String, pub asset_coverage_json: String, pub checkpoints_json: String, + pub supported_actions_json: String, pub messages: Vec, pub draft_cards: Vec, pub operations: Vec, @@ -425,6 +428,126 @@ pub struct CustomWorldAgentOperationProcedureResult { pub error_message: Option, } +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldWorksListInput { + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldPublishBlockerSnapshot { + pub blocker_id: String, + pub code: String, + pub message: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldPublishGateSnapshot { + pub profile_id: String, + pub blockers: Vec, + pub blocker_count: u32, + pub publish_ready: bool, + pub can_enter_world: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldWorkSummarySnapshot { + pub work_id: String, + pub source_type: String, + pub status: String, + pub title: String, + pub subtitle: String, + pub summary: String, + pub cover_image_src: Option, + pub cover_render_mode: Option, + pub cover_character_image_srcs_json: String, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub stage: Option, + pub stage_label: Option, + pub playable_npc_count: u32, + pub landmark_count: u32, + pub role_visual_ready_count: Option, + pub role_animation_ready_count: Option, + pub role_asset_summary_label: Option, + pub session_id: Option, + pub profile_id: Option, + pub can_resume: bool, + pub can_enter_world: bool, + pub blocker_count: u32, + pub publish_ready: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldWorksListResult { + pub ok: bool, + pub items: Vec, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldAgentCardDetailGetInput { + pub session_id: String, + pub owner_user_id: String, + pub card_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldDraftCardDetailSectionSnapshot { + pub section_id: String, + pub label: String, + pub value: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldDraftCardDetailSnapshot { + pub card_id: String, + pub kind: RpgAgentDraftCardKind, + pub title: String, + pub sections: Vec, + pub linked_ids_json: String, + pub locked: bool, + pub editable: bool, + pub editable_section_ids_json: String, + pub warning_messages_json: String, + pub asset_status: Option, + pub asset_status_label: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldDraftCardDetailResult { + pub ok: bool, + pub card: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldAgentActionExecuteInput { + pub session_id: String, + pub owner_user_id: String, + pub operation_id: String, + pub action: String, + pub payload_json: Option, + pub submitted_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CustomWorldAgentActionExecuteResult { + pub ok: bool, + pub operation: Option, + pub error_message: Option, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct CustomWorldPublishedProfileCompileInput { @@ -941,6 +1064,52 @@ pub fn validate_custom_world_agent_operation_get_input( Ok(()) } +pub fn validate_custom_world_works_list_input( + input: &CustomWorldWorksListInput, +) -> Result<(), CustomWorldFieldError> { + if input.owner_user_id.trim().is_empty() { + return Err(CustomWorldFieldError::MissingOwnerUserId); + } + + Ok(()) +} + +pub fn validate_custom_world_agent_card_detail_get_input( + input: &CustomWorldAgentCardDetailGetInput, +) -> Result<(), CustomWorldFieldError> { + if input.session_id.trim().is_empty() { + return Err(CustomWorldFieldError::MissingSessionId); + } + if input.owner_user_id.trim().is_empty() { + return Err(CustomWorldFieldError::MissingOwnerUserId); + } + if input.card_id.trim().is_empty() { + return Err(CustomWorldFieldError::MissingCardId); + } + + Ok(()) +} + +pub fn validate_custom_world_agent_action_execute_input( + input: &CustomWorldAgentActionExecuteInput, +) -> Result<(), CustomWorldFieldError> { + validate_custom_world_agent_operation_get_input(&CustomWorldAgentOperationGetInput { + session_id: input.session_id.clone(), + owner_user_id: input.owner_user_id.clone(), + operation_id: input.operation_id.clone(), + })?; + if input.action.trim().is_empty() { + return Err(CustomWorldFieldError::MissingAction); + } + if let Some(payload_json) = input.payload_json.as_deref() { + if !payload_json.trim().is_empty() { + ensure_json_object(payload_json)?; + } + } + + Ok(()) +} + pub fn validate_custom_world_agent_message_fields( message_id: &str, session_id: &str, @@ -1321,6 +1490,7 @@ impl fmt::Display for CustomWorldFieldError { Self::MissingPhaseLabel => { f.write_str("custom_world_agent_operation.phase_label 不能为空") } + Self::MissingAction => f.write_str("custom_world_agent_action.action 不能为空"), Self::InvalidProgressPercent => f.write_str("progress 必须位于 0~100"), Self::MissingCardId => f.write_str("custom_world_draft_card.card_id 不能为空"), Self::MissingCardTitle => f.write_str("custom_world_draft_card.title 不能为空"), diff --git a/server-rs/crates/module-puzzle/Cargo.toml b/server-rs/crates/module-puzzle/Cargo.toml new file mode 100644 index 00000000..7ed854eb --- /dev/null +++ b/server-rs/crates/module-puzzle/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "module-puzzle" +edition.workspace = true +version.workspace = true +license.workspace = true + +[features] +default = [] +spacetime-types = ["dep:spacetimedb"] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +shared-kernel = { path = "../shared-kernel" } +spacetimedb = { workspace = true, optional = true } diff --git a/server-rs/crates/module-puzzle/src/lib.rs b/server-rs/crates/module-puzzle/src/lib.rs new file mode 100644 index 00000000..ab407774 --- /dev/null +++ b/server-rs/crates/module-puzzle/src/lib.rs @@ -0,0 +1,1538 @@ +use std::{collections::{BTreeMap, BTreeSet, VecDeque}, error::Error, fmt}; + +use serde::{Deserialize, Serialize}; +use shared_kernel::{normalize_required_string, normalize_string_list}; +#[cfg(feature = "spacetime-types")] +use spacetimedb::SpacetimeType; + +pub const PUZZLE_AGENT_SESSION_ID_PREFIX: &str = "puzzle-session-"; +pub const PUZZLE_AGENT_MESSAGE_ID_PREFIX: &str = "puzzle-message-"; +pub const PUZZLE_PROFILE_ID_PREFIX: &str = "puzzle-profile-"; +pub const PUZZLE_RUN_ID_PREFIX: &str = "puzzle-run-"; +pub const PUZZLE_MIN_TAG_COUNT: usize = 3; +pub const PUZZLE_MAX_TAG_COUNT: usize = 6; + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzleAgentStage { + CollectingAnchors, + DraftReady, + ImageRefining, + ReadyToPublish, + Published, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzleAnchorStatus { + Missing, + Inferred, + Confirmed, + Locked, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzleAgentMessageRole { + User, + Assistant, + System, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzleAgentMessageKind { + Chat, + Summary, + ActionResult, + Warning, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzlePublicationStatus { + Draft, + Published, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum PuzzleRuntimeLevelStatus { + Playing, + Cleared, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAnchorItem { + pub key: String, + pub label: String, + pub value: String, + pub status: PuzzleAnchorStatus, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAnchorPack { + pub theme_promise: PuzzleAnchorItem, + pub visual_subject: PuzzleAnchorItem, + pub visual_mood: PuzzleAnchorItem, + pub composition_hooks: PuzzleAnchorItem, + pub tags_and_forbidden: PuzzleAnchorItem, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleCreatorIntent { + pub source_mode: String, + pub raw_messages_summary: String, + pub theme_promise: String, + pub visual_subject: String, + pub visual_mood: Vec, + pub composition_hooks: Vec, + pub theme_tags: Vec, + pub forbidden_directives: Vec, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleGeneratedImageCandidate { + pub candidate_id: String, + pub image_src: String, + pub asset_id: String, + pub prompt: String, + pub actual_prompt: Option, + pub source_type: String, + pub selected: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleResultDraft { + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub forbidden_directives: Vec, + pub creator_intent: Option, + pub anchor_pack: PuzzleAnchorPack, + pub candidates: Vec, + pub selected_candidate_id: Option, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub generation_status: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleResultPreviewBlocker { + pub id: String, + pub code: String, + pub message: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleResultPreviewFinding { + pub id: String, + pub severity: String, + pub code: String, + pub message: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleResultPreviewEnvelope { + pub draft: PuzzleResultDraft, + pub blockers: Vec, + pub quality_findings: Vec, + pub publish_ready: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: PuzzleAgentMessageRole, + pub kind: PuzzleAgentMessageKind, + pub text: String, + pub created_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentSuggestedAction { + pub id: String, + pub action_type: String, + pub label: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: PuzzleAgentStage, + pub anchor_pack: PuzzleAnchorPack, + pub draft: Option, + pub messages: Vec, + pub last_assistant_reply: Option, + pub published_profile_id: Option, + pub suggested_actions: Vec, + pub result_preview: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorkProfile { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub publication_status: PuzzlePublicationStatus, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub play_count: u32, + pub publish_ready: bool, + pub anchor_pack: PuzzleAnchorPack, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleCellPosition { + pub row: u32, + pub col: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzlePieceState { + pub piece_id: String, + pub correct_row: u32, + pub correct_col: u32, + pub current_row: u32, + pub current_col: u32, + pub merged_group_id: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleMergedGroupState { + pub group_id: String, + pub piece_ids: Vec, + pub occupied_cells: Vec, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleBoardSnapshot { + pub rows: u32, + pub cols: u32, + pub pieces: Vec, + pub merged_groups: Vec, + pub selected_piece_id: Option, + pub all_tiles_resolved: bool, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRuntimeLevelSnapshot { + pub run_id: String, + pub level_index: u32, + pub grid_size: u32, + pub profile_id: String, + pub level_name: String, + pub author_display_name: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub board: PuzzleBoardSnapshot, + pub status: PuzzleRuntimeLevelStatus, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunSnapshot { + pub run_id: String, + pub entry_profile_id: String, + pub cleared_level_count: u32, + pub current_level_index: u32, + pub current_grid_size: u32, + pub played_profile_ids: Vec, + pub previous_level_tags: Vec, + pub current_level: Option, + pub recommended_next_profile_id: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentMessageSubmitInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub submitted_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub compiled_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleGeneratedImagesSaveInput { + pub session_id: String, + pub owner_user_id: String, + pub candidates_json: String, + pub saved_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleSelectCoverImageInput { + pub session_id: String, + pub owner_user_id: String, + pub candidate_id: String, + pub selected_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzlePublishInput { + pub session_id: String, + pub owner_user_id: String, + pub work_id: String, + pub profile_id: String, + pub author_display_name: String, + pub level_name: Option, + pub summary: Option, + pub theme_tags: Option>, + pub published_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorksListInput { + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorkGetInput { + pub profile_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorkUpsertInput { + pub profile_id: String, + pub owner_user_id: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub started_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunSwapInput { + pub run_id: String, + pub owner_user_id: String, + pub first_piece_id: String, + pub second_piece_id: String, + pub swapped_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunDragInput { + pub run_id: String, + pub owner_user_id: String, + pub piece_id: String, + pub target_row: u32, + pub target_col: u32, + pub dragged_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunNextLevelInput { + pub run_id: String, + pub owner_user_id: String, + pub advanced_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleAgentSessionProcedureResult { + pub ok: bool, + pub session_json: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorksProcedureResult { + pub ok: bool, + pub items_json: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleWorkProcedureResult { + pub ok: bool, + pub item_json: Option, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PuzzleRunProcedureResult { + pub ok: bool, + pub run_json: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PuzzleFieldError { + MissingText, + MissingSessionId, + MissingProfileId, + MissingRunId, + MissingPieceId, + MissingAuthorDisplayName, + InvalidTagCount, + InvalidGridSize, + InvalidTargetCell, + InvalidOperation, +} + +impl fmt::Display for PuzzleFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingText => write!(f, "必填文本缺失"), + Self::MissingSessionId => write!(f, "session_id 缺失"), + Self::MissingProfileId => write!(f, "profile_id 缺失"), + Self::MissingRunId => write!(f, "run_id 缺失"), + Self::MissingPieceId => write!(f, "piece_id 缺失"), + Self::MissingAuthorDisplayName => write!(f, "author_display_name 缺失"), + Self::InvalidTagCount => write!(f, "标签数量不合法"), + Self::InvalidGridSize => write!(f, "网格规格不合法"), + Self::InvalidTargetCell => write!(f, "目标格子不合法"), + Self::InvalidOperation => write!(f, "操作不合法"), + } + } +} + +impl Error for PuzzleFieldError {} + +impl PuzzleAgentStage { + pub fn as_str(self) -> &'static str { + match self { + Self::CollectingAnchors => "collecting_anchors", + Self::DraftReady => "draft_ready", + Self::ImageRefining => "image_refining", + Self::ReadyToPublish => "ready_to_publish", + Self::Published => "published", + } + } +} + +impl PuzzleAnchorStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Missing => "missing", + Self::Inferred => "inferred", + Self::Confirmed => "confirmed", + Self::Locked => "locked", + } + } +} + +impl PuzzleAgentMessageRole { + pub fn as_str(self) -> &'static str { + match self { + Self::User => "user", + Self::Assistant => "assistant", + Self::System => "system", + } + } +} + +impl PuzzleAgentMessageKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Chat => "chat", + Self::Summary => "summary", + Self::ActionResult => "action_result", + Self::Warning => "warning", + } + } +} + +impl PuzzlePublicationStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Draft => "draft", + Self::Published => "published", + } + } +} + +impl PuzzleRuntimeLevelStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Playing => "playing", + Self::Cleared => "cleared", + } + } +} + +pub fn empty_anchor_pack() -> PuzzleAnchorPack { + PuzzleAnchorPack { + theme_promise: PuzzleAnchorItem { + key: "themePromise".to_string(), + label: "题材承诺".to_string(), + value: String::new(), + status: PuzzleAnchorStatus::Missing, + }, + visual_subject: PuzzleAnchorItem { + key: "visualSubject".to_string(), + label: "画面主体".to_string(), + value: String::new(), + status: PuzzleAnchorStatus::Missing, + }, + visual_mood: PuzzleAnchorItem { + key: "visualMood".to_string(), + label: "视觉气质".to_string(), + value: String::new(), + status: PuzzleAnchorStatus::Missing, + }, + composition_hooks: PuzzleAnchorItem { + key: "compositionHooks".to_string(), + label: "拼图记忆点".to_string(), + value: String::new(), + status: PuzzleAnchorStatus::Missing, + }, + tags_and_forbidden: PuzzleAnchorItem { + key: "tagsAndForbidden".to_string(), + label: "标签与禁忌".to_string(), + value: String::new(), + status: PuzzleAnchorStatus::Missing, + }, + } +} + +pub fn infer_anchor_pack(seed_text: &str, latest_message: Option<&str>) -> PuzzleAnchorPack { + let source = normalize_required_string(latest_message.unwrap_or(seed_text)) + .or_else(|| normalize_required_string(seed_text)) + .unwrap_or_else(|| "童话森林里的发光猫咪遗迹".to_string()); + let mut pack = empty_anchor_pack(); + pack.theme_promise.value = infer_theme_promise(&source); + pack.theme_promise.status = PuzzleAnchorStatus::Inferred; + pack.visual_subject.value = infer_visual_subject(&source); + pack.visual_subject.status = PuzzleAnchorStatus::Inferred; + pack.visual_mood.value = infer_visual_mood(&source); + pack.visual_mood.status = PuzzleAnchorStatus::Inferred; + pack.composition_hooks.value = infer_composition_hooks(&source); + pack.composition_hooks.status = PuzzleAnchorStatus::Inferred; + pack.tags_and_forbidden.value = infer_tags_and_forbidden(&source); + pack.tags_and_forbidden.status = PuzzleAnchorStatus::Inferred; + pack +} + +pub fn build_creator_intent(anchor_pack: &PuzzleAnchorPack, messages: &[PuzzleAgentMessageSnapshot]) -> PuzzleCreatorIntent { + PuzzleCreatorIntent { + source_mode: "agent_chat".to_string(), + raw_messages_summary: messages + .iter() + .rev() + .take(4) + .map(|entry| entry.text.clone()) + .collect::>() + .join(" / "), + theme_promise: anchor_pack.theme_promise.value.clone(), + visual_subject: anchor_pack.visual_subject.value.clone(), + visual_mood: split_phrase_list(&anchor_pack.visual_mood.value), + composition_hooks: split_phrase_list(&anchor_pack.composition_hooks.value), + theme_tags: split_phrase_list(&anchor_pack.tags_and_forbidden.value) + .into_iter() + .take(PUZZLE_MAX_TAG_COUNT) + .collect(), + forbidden_directives: vec![extract_forbidden_directive(&anchor_pack.tags_and_forbidden.value)], + } +} + +pub fn compile_result_draft(anchor_pack: &PuzzleAnchorPack, messages: &[PuzzleAgentMessageSnapshot]) -> PuzzleResultDraft { + let creator_intent = build_creator_intent(anchor_pack, messages); + let normalized_tags = normalize_theme_tags(creator_intent.theme_tags.clone()); + let level_name = build_level_name(anchor_pack, &normalized_tags); + PuzzleResultDraft { + level_name, + summary: format!( + "{},主体是{},氛围偏{}。", + fallback_text(&anchor_pack.theme_promise.value, "梦幻题材"), + fallback_text(&anchor_pack.visual_subject.value, "画面主体"), + fallback_text(&anchor_pack.visual_mood.value, "温暖") + ), + theme_tags: normalized_tags, + forbidden_directives: creator_intent.forbidden_directives.clone(), + creator_intent: Some(creator_intent), + anchor_pack: anchor_pack.clone(), + candidates: Vec::new(), + selected_candidate_id: None, + cover_image_src: None, + cover_asset_id: None, + generation_status: "idle".to_string(), + } +} + +pub fn build_generated_candidates( + session_id: &str, + prompt_text: Option<&str>, + draft: &PuzzleResultDraft, + candidate_count: u32, + now_micros: i64, +) -> Result, PuzzleFieldError> { + let session_id = + normalize_required_string(session_id).ok_or(PuzzleFieldError::MissingSessionId)?; + let count = candidate_count.max(1).min(2); + let prompt = normalize_required_string(prompt_text.unwrap_or(&draft.summary)) + .unwrap_or_else(|| draft.summary.clone()); + + Ok((0..count) + .map(|index| { + let candidate_seed = now_micros + i64::from(index); + let candidate_id = format!("{session_id}-candidate-{}", index + 1); + PuzzleGeneratedImageCandidate { + candidate_id: candidate_id.clone(), + image_src: format!( + "/generated-puzzle-covers/{session_id}/{candidate_seed}/cover.svg" + ), + asset_id: format!("puzzle-cover-{candidate_seed}"), + prompt: prompt.clone(), + actual_prompt: Some(prompt.clone()), + source_type: "generated".to_string(), + selected: index == 0, + } + }) + .collect()) +} + +pub fn apply_selected_candidate( + mut draft: PuzzleResultDraft, + candidate_id: &str, +) -> Result { + let candidate_id = + normalize_required_string(candidate_id).ok_or(PuzzleFieldError::MissingText)?; + let mut selected_cover_image_src = None; + let mut selected_cover_asset_id = None; + let mut matched = false; + + for candidate in &mut draft.candidates { + candidate.selected = candidate.candidate_id == candidate_id; + if candidate.selected { + matched = true; + selected_cover_image_src = Some(candidate.image_src.clone()); + selected_cover_asset_id = Some(candidate.asset_id.clone()); + } + } + + if !matched { + return Err(PuzzleFieldError::InvalidOperation); + } + + draft.selected_candidate_id = Some(candidate_id); + draft.cover_image_src = selected_cover_image_src; + draft.cover_asset_id = selected_cover_asset_id; + draft.generation_status = "ready".to_string(); + Ok(draft) +} + +pub fn build_result_preview(draft: &PuzzleResultDraft, author_display_name: Option<&str>) -> PuzzleResultPreviewEnvelope { + let blockers = validate_publish_requirements(draft, author_display_name); + PuzzleResultPreviewEnvelope { + draft: draft.clone(), + blockers, + quality_findings: Vec::new(), + publish_ready: validate_publish_requirements(draft, author_display_name).is_empty(), + } +} + +pub fn validate_publish_requirements( + draft: &PuzzleResultDraft, + author_display_name: Option<&str>, +) -> Vec { + let mut blockers = Vec::new(); + if normalize_required_string(&draft.level_name).is_none() { + blockers.push(PuzzleResultPreviewBlocker { + id: "missing-level-name".to_string(), + code: "MISSING_LEVEL_NAME".to_string(), + message: "关卡名不能为空".to_string(), + }); + } + if draft.cover_image_src.as_deref().map(str::trim).unwrap_or("").is_empty() { + blockers.push(PuzzleResultPreviewBlocker { + id: "missing-cover-image".to_string(), + code: "MISSING_COVER_IMAGE".to_string(), + message: "正式拼图图片尚未确定".to_string(), + }); + } + if draft.theme_tags.len() < PUZZLE_MIN_TAG_COUNT || draft.theme_tags.len() > PUZZLE_MAX_TAG_COUNT { + blockers.push(PuzzleResultPreviewBlocker { + id: "invalid-tag-count".to_string(), + code: "INVALID_TAG_COUNT".to_string(), + message: "正式标签数量必须在 3 到 6 之间".to_string(), + }); + } + if normalize_required_string(author_display_name.unwrap_or("")).is_none() { + blockers.push(PuzzleResultPreviewBlocker { + id: "missing-author".to_string(), + code: "MISSING_AUTHOR".to_string(), + message: "作者信息不可读".to_string(), + }); + } + blockers +} + +pub fn create_work_profile( + work_id: String, + profile_id: String, + owner_user_id: String, + source_session_id: Option, + author_display_name: String, + draft: &PuzzleResultDraft, + updated_at_micros: i64, +) -> Result { + let author_display_name = normalize_required_string(author_display_name) + .ok_or(PuzzleFieldError::MissingAuthorDisplayName)?; + let preview = build_result_preview(draft, Some(&author_display_name)); + Ok(PuzzleWorkProfile { + work_id, + profile_id, + owner_user_id, + source_session_id, + author_display_name, + level_name: draft.level_name.clone(), + summary: draft.summary.clone(), + theme_tags: normalize_theme_tags(draft.theme_tags.clone()), + cover_image_src: draft.cover_image_src.clone(), + cover_asset_id: draft.cover_asset_id.clone(), + publication_status: PuzzlePublicationStatus::Draft, + updated_at_micros, + published_at_micros: None, + play_count: 0, + publish_ready: preview.publish_ready, + anchor_pack: draft.anchor_pack.clone(), + }) +} + +pub fn publish_work_profile( + mut profile: PuzzleWorkProfile, + draft: &PuzzleResultDraft, + published_at_micros: i64, +) -> Result { + if !validate_publish_requirements(draft, Some(&profile.author_display_name)).is_empty() { + return Err(PuzzleFieldError::InvalidOperation); + } + profile.level_name = draft.level_name.clone(); + profile.summary = draft.summary.clone(); + profile.theme_tags = normalize_theme_tags(draft.theme_tags.clone()); + profile.cover_image_src = draft.cover_image_src.clone(); + profile.cover_asset_id = draft.cover_asset_id.clone(); + profile.publication_status = PuzzlePublicationStatus::Published; + profile.publish_ready = true; + profile.updated_at_micros = published_at_micros; + profile.published_at_micros = Some(published_at_micros); + Ok(profile) +} + +/// 在发布前把结果页的轻量编辑字段覆盖回草稿真相。 +/// 这里只允许覆盖 PRD 明确要求的关卡名、摘要与标签,不额外扩到更多结果页元数据。 +pub fn apply_publish_overrides_to_draft( + draft: &PuzzleResultDraft, + level_name: Option, + summary: Option, + theme_tags: Option>, +) -> Result { + let mut next_draft = draft.clone(); + + if let Some(next_level_name) = level_name + && let Some(normalized_level_name) = normalize_required_string(&next_level_name) + { + next_draft.level_name = normalized_level_name; + } + + if let Some(next_summary) = summary + && let Some(normalized_summary) = normalize_required_string(&next_summary) + { + next_draft.summary = normalized_summary; + } + + if let Some(next_theme_tags) = theme_tags { + let normalized_theme_tags = normalize_theme_tags(next_theme_tags); + if normalized_theme_tags.len() < PUZZLE_MIN_TAG_COUNT + || normalized_theme_tags.len() > PUZZLE_MAX_TAG_COUNT + { + return Err(PuzzleFieldError::InvalidTagCount); + } + next_draft.theme_tags = normalized_theme_tags; + } + + Ok(next_draft) +} + +pub fn resolve_puzzle_grid_size(cleared_level_count: u32) -> u32 { + if cleared_level_count >= 3 { 4 } else { 3 } +} + +pub fn build_initial_board(grid_size: u32) -> Result { + if !matches!(grid_size, 3 | 4) { + return Err(PuzzleFieldError::InvalidGridSize); + } + + let total = grid_size * grid_size; + let mut positions = (0..total) + .map(|index| PuzzleCellPosition { + row: index / grid_size, + col: index % grid_size, + }) + .collect::>(); + + if total > 1 { + positions.rotate_left(1); + } + + let pieces = (0..total) + .map(|index| { + let correct_row = index / grid_size; + let correct_col = index % grid_size; + let current = &positions[index as usize]; + PuzzlePieceState { + piece_id: format!("piece-{index}"), + correct_row, + correct_col, + current_row: current.row, + current_col: current.col, + merged_group_id: None, + } + }) + .collect::>(); + + Ok(rebuild_board_snapshot(grid_size, pieces, None)) +} + +pub fn start_run( + run_id: String, + entry_profile: &PuzzleWorkProfile, + cleared_level_count: u32, +) -> Result { + let grid_size = resolve_puzzle_grid_size(cleared_level_count); + let board = build_initial_board(grid_size)?; + Ok(PuzzleRunSnapshot { + run_id: run_id.clone(), + entry_profile_id: entry_profile.profile_id.clone(), + cleared_level_count, + current_level_index: cleared_level_count + 1, + current_grid_size: grid_size, + played_profile_ids: vec![entry_profile.profile_id.clone()], + previous_level_tags: entry_profile.theme_tags.clone(), + current_level: Some(PuzzleRuntimeLevelSnapshot { + run_id, + level_index: cleared_level_count + 1, + grid_size, + profile_id: entry_profile.profile_id.clone(), + level_name: entry_profile.level_name.clone(), + author_display_name: entry_profile.author_display_name.clone(), + theme_tags: entry_profile.theme_tags.clone(), + cover_image_src: entry_profile.cover_image_src.clone(), + board, + status: PuzzleRuntimeLevelStatus::Playing, + }), + recommended_next_profile_id: None, + }) +} + +pub fn swap_pieces( + run: &PuzzleRunSnapshot, + first_piece_id: &str, + second_piece_id: &str, +) -> Result { + let first_piece_id = + normalize_required_string(first_piece_id).ok_or(PuzzleFieldError::MissingPieceId)?; + let second_piece_id = + normalize_required_string(second_piece_id).ok_or(PuzzleFieldError::MissingPieceId)?; + let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?; + if current_level.status == PuzzleRuntimeLevelStatus::Cleared { + return Err(PuzzleFieldError::InvalidOperation); + } + let mut pieces = current_level.board.pieces.clone(); + let first_index = pieces + .iter() + .position(|piece| piece.piece_id == first_piece_id) + .ok_or(PuzzleFieldError::MissingPieceId)?; + let second_index = pieces + .iter() + .position(|piece| piece.piece_id == second_piece_id) + .ok_or(PuzzleFieldError::MissingPieceId)?; + + let (first_row, first_col) = (pieces[first_index].current_row, pieces[first_index].current_col); + let (second_row, second_col) = + (pieces[second_index].current_row, pieces[second_index].current_col); + pieces[first_index].current_row = second_row; + pieces[first_index].current_col = second_col; + pieces[second_index].current_row = first_row; + pieces[second_index].current_col = first_col; + + let next_board = rebuild_board_snapshot(current_level.grid_size, pieces, None); + Ok(with_next_board(run, next_board)) +} + +pub fn drag_piece_or_group( + run: &PuzzleRunSnapshot, + piece_id: &str, + target_row: u32, + target_col: u32, +) -> Result { + let piece_id = normalize_required_string(piece_id).ok_or(PuzzleFieldError::MissingPieceId)?; + let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?; + if current_level.status == PuzzleRuntimeLevelStatus::Cleared { + return Err(PuzzleFieldError::InvalidOperation); + } + let grid_size = current_level.grid_size; + if target_row >= grid_size || target_col >= grid_size { + return Err(PuzzleFieldError::InvalidTargetCell); + } + + let mut pieces = current_level.board.pieces.clone(); + let piece_index = pieces + .iter() + .position(|piece| piece.piece_id == piece_id) + .ok_or(PuzzleFieldError::MissingPieceId)?; + let source_group_id = pieces[piece_index].merged_group_id.clone(); + + match source_group_id { + Some(group_id) => drag_group(&mut pieces, &group_id, target_row, target_col, grid_size)?, + None => drag_single_piece(&mut pieces, piece_index, target_row, target_col)?, + } + + let next_board = rebuild_board_snapshot(grid_size, pieces, None); + Ok(with_next_board(run, next_board)) +} + +pub fn advance_next_level( + run: &PuzzleRunSnapshot, + next_profile: &PuzzleWorkProfile, +) -> Result { + let current_level = run.current_level.clone().ok_or(PuzzleFieldError::InvalidOperation)?; + if current_level.status != PuzzleRuntimeLevelStatus::Cleared { + return Err(PuzzleFieldError::InvalidOperation); + } + + let next_cleared_count = run.cleared_level_count; + let next_grid_size = resolve_puzzle_grid_size(next_cleared_count); + let next_board = build_initial_board(next_grid_size)?; + let mut played_profile_ids = run.played_profile_ids.clone(); + played_profile_ids.push(next_profile.profile_id.clone()); + + Ok(PuzzleRunSnapshot { + run_id: run.run_id.clone(), + entry_profile_id: run.entry_profile_id.clone(), + cleared_level_count: next_cleared_count, + current_level_index: run.current_level_index + 1, + current_grid_size: next_grid_size, + played_profile_ids, + previous_level_tags: next_profile.theme_tags.clone(), + current_level: Some(PuzzleRuntimeLevelSnapshot { + run_id: run.run_id.clone(), + level_index: run.current_level_index + 1, + grid_size: next_grid_size, + profile_id: next_profile.profile_id.clone(), + level_name: next_profile.level_name.clone(), + author_display_name: next_profile.author_display_name.clone(), + theme_tags: next_profile.theme_tags.clone(), + cover_image_src: next_profile.cover_image_src.clone(), + board: next_board, + status: PuzzleRuntimeLevelStatus::Playing, + }), + recommended_next_profile_id: None, + }) +} + +pub fn select_next_profile<'a>( + current_profile: &PuzzleWorkProfile, + played_profile_ids: &[String], + candidates: &'a [PuzzleWorkProfile], +) -> Option<&'a PuzzleWorkProfile> { + let mut available = candidates + .iter() + .filter(|candidate| { + candidate.publication_status == PuzzlePublicationStatus::Published + && candidate.cover_image_src.is_some() + && !candidate.theme_tags.is_empty() + && candidate.profile_id != current_profile.profile_id + }) + .collect::>(); + + let has_unplayed = available + .iter() + .any(|candidate| !played_profile_ids.contains(&candidate.profile_id)); + + if has_unplayed { + available.retain(|candidate| !played_profile_ids.contains(&candidate.profile_id)); + } else if let Some(last_played) = played_profile_ids.last() { + available.retain(|candidate| candidate.profile_id != *last_played); + } + + available.into_iter().max_by(|left, right| { + let left_score = recommendation_score(current_profile, left); + let right_score = recommendation_score(current_profile, right); + left_score + .partial_cmp(&right_score) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| { + tag_similarity_score(¤t_profile.theme_tags, &left.theme_tags) + .partial_cmp(&tag_similarity_score(¤t_profile.theme_tags, &right.theme_tags)) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .then_with(|| right.play_count.cmp(&left.play_count)) + .then_with(|| left.updated_at_micros.cmp(&right.updated_at_micros)) + }) +} + +pub fn recommendation_score(current_profile: &PuzzleWorkProfile, candidate: &PuzzleWorkProfile) -> f32 { + let tag_similarity = tag_similarity_score(¤t_profile.theme_tags, &candidate.theme_tags); + let same_author_score = if current_profile.owner_user_id == candidate.owner_user_id { + 1.0 + } else { + 0.0 + }; + tag_similarity * 0.7 + same_author_score * 0.3 +} + +pub fn tag_similarity_score(left_tags: &[String], right_tags: &[String]) -> f32 { + let left_set = normalize_theme_tags(left_tags.to_vec()).into_iter().collect::>(); + let right_set = normalize_theme_tags(right_tags.to_vec()).into_iter().collect::>(); + if left_set.is_empty() && right_set.is_empty() { + return 0.0; + } + let intersection = left_set.intersection(&right_set).count() as f32; + let union = left_set.union(&right_set).count() as f32; + if union <= f32::EPSILON { + 0.0 + } else { + intersection / union + } +} + +pub fn normalize_theme_tags(tags: Vec) -> Vec { + let alias_map = BTreeMap::from([ + ("蒸汽", "蒸汽城市"), + ("蒸汽朋克", "蒸汽城市"), + ("遗迹", "神庙遗迹"), + ("森林", "童话森林"), + ("夜雨", "雨夜"), + ("发光猫", "猫咪"), + ]); + + let mut normalized = normalize_string_list(tags) + .into_iter() + .flat_map(|value| split_phrase_list(&value)) + .map(|value| { + alias_map + .get(value.as_str()) + .map(|alias| (*alias).to_string()) + .unwrap_or(value) + }) + .collect::>(); + normalized.sort(); + normalized.dedup(); + normalized.into_iter().take(PUZZLE_MAX_TAG_COUNT).collect() +} + +fn infer_theme_promise(source: &str) -> String { + if source.contains("神庙") { + "探索遗迹中的奇幻想象".to_string() + } else if source.contains("雨") { + "在雨夜中寻找视觉线索".to_string() + } else if source.contains("猫") { + "一眼记住可爱的猫咪奇景".to_string() + } else { + "用一张强识别度画面承诺幻想题材".to_string() + } +} + +fn infer_visual_subject(source: &str) -> String { + if source.contains("猫") { + "发光猫咪".to_string() + } else if source.contains("神庙") { + "巨大遗迹入口".to_string() + } else if source.contains("城市") { + "蒸汽城市核心地标".to_string() + } else { + "画面中央的核心主体".to_string() + } +} + +fn infer_visual_mood(source: &str) -> String { + if source.contains("悬疑") { + "悬疑、静谧".to_string() + } else if source.contains("温暖") { + "温暖、柔和".to_string() + } else if source.contains("机械") { + "机械、奇诡".to_string() + } else { + "梦幻、清晰".to_string() + } +} + +fn infer_composition_hooks(source: &str) -> String { + if source.contains("塔") { + "高塔轮廓、纵向构图、亮色焦点".to_string() + } else if source.contains("遗迹") { + "入口轮廓、对称台阶、地标雕像".to_string() + } else { + "主体轮廓、色块分区、地标元素".to_string() + } +} + +fn infer_tags_and_forbidden(source: &str) -> String { + if source.contains("神庙") { + "神庙遗迹、童话森林、雨夜;禁止标题字".to_string() + } else if source.contains("猫") { + "猫咪、童话森林、发光;禁止水印".to_string() + } else { + "蒸汽城市、雨夜、奇幻;禁止按钮".to_string() + } +} + +fn extract_forbidden_directive(source: &str) -> String { + if let Some((_, tail)) = source.split_once(';') { + return normalize_required_string(tail).unwrap_or_else(|| "禁止标题字".to_string()); + } + "禁止标题字".to_string() +} + +fn build_level_name(anchor_pack: &PuzzleAnchorPack, normalized_tags: &[String]) -> String { + if let Some(tag) = normalized_tags.first() { + return format!("{tag}拼图"); + } + if let Some(subject) = normalize_required_string(&anchor_pack.visual_subject.value) { + return subject.chars().take(8).collect::(); + } + "奇景拼图".to_string() +} + +fn fallback_text(value: &str, fallback: &str) -> String { + normalize_required_string(value).unwrap_or_else(|| fallback.to_string()) +} + +fn split_phrase_list(value: &str) -> Vec { + value + .replace(',', ",") + .replace('、', ",") + .replace(';', ",") + .split(',') + .filter_map(normalize_required_string) + .collect() +} + +fn rebuild_board_snapshot( + grid_size: u32, + mut pieces: Vec, + selected_piece_id: Option, +) -> PuzzleBoardSnapshot { + let merged_groups = resolve_merged_groups(&pieces); + let group_by_piece = merged_groups + .iter() + .flat_map(|group| { + group.piece_ids.iter().cloned().map(|piece_id| (piece_id, group.group_id.clone())) + }) + .collect::>(); + + for piece in &mut pieces { + piece.merged_group_id = group_by_piece.get(&piece.piece_id).cloned(); + } + + let all_tiles_resolved = pieces.iter().all(|piece| { + piece.correct_row == piece.current_row && piece.correct_col == piece.current_col + }); + + PuzzleBoardSnapshot { + rows: grid_size, + cols: grid_size, + pieces, + merged_groups, + selected_piece_id, + all_tiles_resolved, + } +} + +fn resolve_merged_groups(pieces: &[PuzzlePieceState]) -> Vec { + let pieces_by_cell = pieces + .iter() + .map(|piece| ((piece.current_row, piece.current_col), piece)) + .collect::>(); + let pieces_by_id = pieces + .iter() + .map(|piece| (piece.piece_id.clone(), piece)) + .collect::>(); + let mut visited = BTreeSet::new(); + let mut groups = Vec::new(); + + for piece in pieces { + if visited.contains(&piece.piece_id) { + continue; + } + + let mut queue = VecDeque::from([piece.piece_id.clone()]); + let mut collected_ids = Vec::new(); + + while let Some(current_piece_id) = queue.pop_front() { + if !visited.insert(current_piece_id.clone()) { + continue; + } + let current_piece = match pieces_by_id.get(¤t_piece_id) { + Some(value) => *value, + None => continue, + }; + collected_ids.push(current_piece_id.clone()); + + for (neighbor_row, neighbor_col) in neighbor_cells(current_piece.current_row, current_piece.current_col) { + if let Some(neighbor_piece) = pieces_by_cell.get(&(neighbor_row, neighbor_col)) + && are_correct_neighbors(current_piece, neighbor_piece) + { + queue.push_back(neighbor_piece.piece_id.clone()); + } + } + } + + if collected_ids.len() <= 1 { + continue; + } + + let occupied_cells = collected_ids + .iter() + .filter_map(|piece_id| pieces_by_id.get(piece_id).copied()) + .map(|piece| PuzzleCellPosition { + row: piece.current_row, + col: piece.current_col, + }) + .collect::>(); + + groups.push(PuzzleMergedGroupState { + group_id: format!("group-{}", groups.len() + 1), + piece_ids: collected_ids, + occupied_cells, + }); + } + + groups +} + +fn neighbor_cells(row: u32, col: u32) -> Vec<(u32, u32)> { + let mut neighbors = Vec::new(); + if row > 0 { + neighbors.push((row - 1, col)); + } + neighbors.push((row + 1, col)); + if col > 0 { + neighbors.push((row, col - 1)); + } + neighbors.push((row, col + 1)); + neighbors +} + +fn are_correct_neighbors(left: &PuzzlePieceState, right: &PuzzlePieceState) -> bool { + let current_row_delta = right.current_row as i32 - left.current_row as i32; + let current_col_delta = right.current_col as i32 - left.current_col as i32; + let correct_row_delta = right.correct_row as i32 - left.correct_row as i32; + let correct_col_delta = right.correct_col as i32 - left.correct_col as i32; + (current_row_delta.abs() + current_col_delta.abs()) == 1 + && current_row_delta == correct_row_delta + && current_col_delta == correct_col_delta +} + +fn drag_single_piece( + pieces: &mut [PuzzlePieceState], + piece_index: usize, + target_row: u32, + target_col: u32, +) -> Result<(), PuzzleFieldError> { + let target_index = pieces + .iter() + .position(|piece| piece.current_row == target_row && piece.current_col == target_col) + .ok_or(PuzzleFieldError::InvalidTargetCell)?; + + if let Some(target_group_id) = pieces[target_index].merged_group_id.clone() { + for piece in pieces.iter_mut().filter(|piece| piece.merged_group_id.as_deref() == Some(target_group_id.as_str())) { + piece.merged_group_id = None; + } + } + + let (source_row, source_col) = (pieces[piece_index].current_row, pieces[piece_index].current_col); + pieces[piece_index].current_row = target_row; + pieces[piece_index].current_col = target_col; + if target_index != piece_index { + pieces[target_index].current_row = source_row; + pieces[target_index].current_col = source_col; + } + Ok(()) +} + +fn drag_group( + pieces: &mut [PuzzlePieceState], + group_id: &str, + target_row: u32, + target_col: u32, + grid_size: u32, +) -> Result<(), PuzzleFieldError> { + let group_indices = pieces + .iter() + .enumerate() + .filter_map(|(index, piece)| (piece.merged_group_id.as_deref() == Some(group_id)).then_some(index)) + .collect::>(); + if group_indices.is_empty() { + return Err(PuzzleFieldError::InvalidOperation); + } + + let anchor_piece = &pieces[group_indices[0]]; + let row_offset = target_row as i32 - anchor_piece.current_row as i32; + let col_offset = target_col as i32 - anchor_piece.current_col as i32; + let mut target_positions = Vec::new(); + for &index in &group_indices { + let next_row = pieces[index].current_row as i32 + row_offset; + let next_col = pieces[index].current_col as i32 + col_offset; + if next_row < 0 || next_col < 0 || next_row >= grid_size as i32 || next_col >= grid_size as i32 { + return Err(PuzzleFieldError::InvalidTargetCell); + } + target_positions.push((index, next_row as u32, next_col as u32)); + } + + let moving_piece_ids = group_indices + .iter() + .map(|index| pieces[*index].piece_id.clone()) + .collect::>(); + let source_positions = group_indices + .iter() + .map(|index| (pieces[*index].current_row, pieces[*index].current_col)) + .collect::>(); + + for (index, next_row, next_col) in &target_positions { + if let Some(target_piece_index) = pieces.iter().position(|piece| { + piece.current_row == *next_row + && piece.current_col == *next_col + && !moving_piece_ids.contains(&piece.piece_id) + }) { + let fallback = source_positions + .iter() + .find(|position| { + !target_positions + .iter() + .any(|(_, row, col)| row == &position.0 && col == &position.1) + }) + .copied() + .ok_or(PuzzleFieldError::InvalidOperation)?; + pieces[target_piece_index].merged_group_id = None; + pieces[target_piece_index].current_row = fallback.0; + pieces[target_piece_index].current_col = fallback.1; + } + pieces[*index].current_row = *next_row; + pieces[*index].current_col = *next_col; + } + + Ok(()) +} + +fn with_next_board(run: &PuzzleRunSnapshot, next_board: PuzzleBoardSnapshot) -> PuzzleRunSnapshot { + let mut next_run = run.clone(); + let is_cleared = next_board.all_tiles_resolved; + let next_level_status = if is_cleared { + PuzzleRuntimeLevelStatus::Cleared + } else { + PuzzleRuntimeLevelStatus::Playing + }; + + if let Some(current_level) = next_run.current_level.as_mut() { + current_level.board = next_board; + current_level.status = next_level_status; + } + + if is_cleared { + next_run.cleared_level_count += 1; + } + next_run +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build_published_profile(profile_id: &str, owner_user_id: &str, tags: Vec<&str>) -> PuzzleWorkProfile { + PuzzleWorkProfile { + work_id: format!("work-{profile_id}"), + profile_id: profile_id.to_string(), + owner_user_id: owner_user_id.to_string(), + source_session_id: None, + author_display_name: "作者".to_string(), + level_name: format!("{profile_id} 关"), + summary: "summary".to_string(), + theme_tags: tags.into_iter().map(|value| value.to_string()).collect(), + cover_image_src: Some("/cover.png".to_string()), + cover_asset_id: Some("asset-1".to_string()), + publication_status: PuzzlePublicationStatus::Published, + updated_at_micros: 100, + published_at_micros: Some(100), + play_count: 0, + publish_ready: true, + anchor_pack: empty_anchor_pack(), + } + } + + #[test] + fn resolve_grid_size_matches_prd() { + assert_eq!(resolve_puzzle_grid_size(0), 3); + assert_eq!(resolve_puzzle_grid_size(2), 3); + assert_eq!(resolve_puzzle_grid_size(3), 4); + } + + #[test] + fn normalize_theme_tags_dedups_aliases() { + assert_eq!( + normalize_theme_tags(vec![ + "蒸汽".to_string(), + "蒸汽朋克".to_string(), + "雨夜".to_string(), + "雨夜".to_string() + ]), + vec!["蒸汽城市".to_string(), "雨夜".to_string()] + ); + } + + #[test] + fn tag_similarity_score_uses_jaccard() { + let score = tag_similarity_score( + &["蒸汽城市".to_string(), "雨夜".to_string()], + &["蒸汽城市".to_string(), "猫咪".to_string()], + ); + assert!((score - 0.3333).abs() < 0.01); + } + + #[test] + fn select_next_profile_prefers_same_tags_and_author() { + let current = build_published_profile("a", "owner-a", vec!["蒸汽城市", "雨夜"]); + let candidates = vec![ + build_published_profile("b", "owner-a", vec!["蒸汽城市", "雨夜"]), + build_published_profile("c", "owner-c", vec!["猫咪", "森林"]), + ]; + let selected = select_next_profile(¤t, &["a".to_string()], &candidates) + .expect("should select"); + assert_eq!(selected.profile_id, "b"); + } + + #[test] + fn swap_pieces_marks_cleared_when_back_to_origin() { + let profile = build_published_profile("entry", "owner-a", vec!["蒸汽城市", "雨夜", "猫咪"]); + let run = start_run("run-1".to_string(), &profile, 0).expect("run"); + let current_level = run.current_level.clone().expect("level"); + let first_piece = current_level.board.pieces[0].clone(); + let second_piece = current_level.board.pieces[1].clone(); + let swapped = swap_pieces(&run, &first_piece.piece_id, &second_piece.piece_id).expect("swap"); + assert_eq!(swapped.current_level.as_ref().expect("level").board.pieces.len(), 9); + } + + #[test] + fn apply_publish_overrides_updates_draft_truth() { + let anchor_pack = infer_anchor_pack("雨夜猫咪神庙", Some("雨夜猫咪神庙")); + let draft = compile_result_draft(&anchor_pack, &[]); + + let updated = apply_publish_overrides_to_draft( + &draft, + Some("雨夜猫塔".to_string()), + Some("一张更聚焦猫咪塔楼的夜景拼图。".to_string()), + Some(vec![ + "雨夜".to_string(), + "猫咪".to_string(), + "遗迹".to_string(), + ]), + ) + .expect("publish overrides should succeed"); + + assert_eq!(updated.level_name, "雨夜猫塔"); + assert_eq!(updated.summary, "一张更聚焦猫咪塔楼的夜景拼图。"); + assert_eq!( + updated.theme_tags, + vec![ + "猫咪".to_string(), + "神庙遗迹".to_string(), + "雨夜".to_string() + ] + ); + } + + #[test] + fn apply_publish_overrides_rejects_invalid_tag_count() { + let anchor_pack = infer_anchor_pack("蒸汽城市", Some("蒸汽城市")); + let draft = compile_result_draft(&anchor_pack, &[]); + let error = apply_publish_overrides_to_draft( + &draft, + None, + None, + Some(vec!["蒸汽".to_string()]), + ) + .expect_err("invalid tag count should fail"); + + assert_eq!(error, PuzzleFieldError::InvalidTagCount); + } +} diff --git a/server-rs/crates/shared-contracts/src/big_fish.rs b/server-rs/crates/shared-contracts/src/big_fish.rs new file mode 100644 index 00000000..3f7eecc7 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/big_fish.rs @@ -0,0 +1,238 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CreateBigFishSessionRequest { + #[serde(default)] + pub seed_text: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SendBigFishMessageRequest { + pub client_message_id: String, + pub text: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ExecuteBigFishActionRequest { + pub action: String, + #[serde(default)] + pub level: Option, + #[serde(default)] + pub motion_key: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SubmitBigFishInputRequest { + pub x: f32, + pub y: f32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishAnchorItemResponse { + pub key: String, + pub label: String, + pub value: String, + pub status: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishAnchorPackResponse { + pub gameplay_promise: BigFishAnchorItemResponse, + pub ecology_visual_theme: BigFishAnchorItemResponse, + pub growth_ladder: BigFishAnchorItemResponse, + pub risk_tempo: BigFishAnchorItemResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishLevelBlueprintResponse { + pub level: u32, + pub name: String, + pub one_line_fantasy: String, + pub silhouette_direction: String, + pub size_ratio: f32, + pub visual_prompt_seed: String, + pub motion_prompt_seed: String, + pub merge_source_level: Option, + pub prey_window: Vec, + pub threat_window: Vec, + pub is_final_level: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishBackgroundBlueprintResponse { + pub theme: String, + pub color_mood: String, + pub foreground_hints: String, + pub midground_composition: String, + pub background_depth: String, + pub safe_play_area_hint: String, + pub spawn_edge_hint: String, + pub background_prompt_seed: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishRuntimeParamsResponse { + pub level_count: u32, + pub merge_count_per_upgrade: u32, + pub spawn_target_count: u32, + pub leader_move_speed: f32, + pub follower_catch_up_speed: f32, + pub offscreen_cull_seconds: f32, + pub prey_spawn_delta_levels: Vec, + pub threat_spawn_delta_levels: Vec, + pub win_level: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishGameDraftResponse { + pub title: String, + pub subtitle: String, + pub core_fun: String, + pub ecology_theme: String, + pub levels: Vec, + pub background: BigFishBackgroundBlueprintResponse, + pub runtime_params: BigFishRuntimeParamsResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishAgentMessageResponse { + pub id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishAssetSlotResponse { + pub slot_id: String, + pub asset_kind: String, + pub level: Option, + pub motion_key: Option, + pub status: String, + pub asset_url: Option, + pub prompt_snapshot: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishAssetCoverageResponse { + pub level_main_image_ready_count: u32, + pub level_motion_ready_count: u32, + pub background_ready: bool, + pub required_level_count: u32, + pub publish_ready: bool, + pub blockers: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishSessionSnapshotResponse { + pub session_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub anchor_pack: BigFishAnchorPackResponse, + pub draft: Option, + pub asset_slots: Vec, + pub asset_coverage: BigFishAssetCoverageResponse, + pub messages: Vec, + pub last_assistant_reply: Option, + pub publish_ready: bool, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishSessionResponse { + pub session: BigFishSessionSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishActionResponse { + pub session: BigFishSessionSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishVector2Response { + pub x: f32, + pub y: f32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishRuntimeEntityResponse { + pub entity_id: String, + pub level: u32, + pub position: BigFishVector2Response, + pub radius: f32, + pub offscreen_seconds: f32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishRuntimeSnapshotResponse { + pub run_id: String, + pub session_id: String, + pub status: String, + pub tick: u64, + pub player_level: u32, + pub win_level: u32, + pub leader_entity_id: Option, + pub owned_entities: Vec, + pub wild_entities: Vec, + pub camera_center: BigFishVector2Response, + pub last_input: BigFishVector2Response, + pub event_log: Vec, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BigFishRunResponse { + pub run: BigFishRuntimeSnapshotResponse, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn big_fish_session_request_uses_camel_case() { + let payload = serde_json::to_value(CreateBigFishSessionRequest { + seed_text: Some("深海机械鱼".to_string()), + }) + .expect("payload should serialize"); + + assert_eq!(payload, json!({ "seedText": "深海机械鱼" })); + } + + #[test] + fn big_fish_action_request_uses_camel_case() { + let payload = serde_json::to_value(ExecuteBigFishActionRequest { + action: "big_fish_generate_level_motion".to_string(), + level: Some(3), + motion_key: Some("move_swim".to_string()), + }) + .expect("payload should serialize"); + + assert_eq!(payload["motionKey"], json!("move_swim")); + assert_eq!(payload["level"], json!(3)); + } +} diff --git a/server-rs/crates/shared-contracts/src/lib.rs b/server-rs/crates/shared-contracts/src/lib.rs index 0cc3d258..05ced81d 100644 --- a/server-rs/crates/shared-contracts/src/lib.rs +++ b/server-rs/crates/shared-contracts/src/lib.rs @@ -2,7 +2,12 @@ pub mod ai; pub mod api; pub mod assets; pub mod auth; +pub mod big_fish; pub mod llm; +pub mod puzzle_agent; +pub mod puzzle_gallery; +pub mod puzzle_runtime; +pub mod puzzle_works; pub mod runtime; pub mod runtime_story; pub mod story; diff --git a/server-rs/crates/shared-contracts/src/puzzle_agent.rs b/server-rs/crates/shared-contracts/src/puzzle_agent.rs new file mode 100644 index 00000000..595fe98d --- /dev/null +++ b/server-rs/crates/shared-contracts/src/puzzle_agent.rs @@ -0,0 +1,188 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CreatePuzzleAgentSessionRequest { + #[serde(default)] + pub seed_text: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SendPuzzleAgentMessageRequest { + pub client_message_id: String, + pub text: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ExecutePuzzleAgentActionRequest { + pub action: String, + #[serde(default)] + pub prompt_text: Option, + #[serde(default)] + pub candidate_count: Option, + #[serde(default)] + pub candidate_id: Option, + #[serde(default)] + pub level_name: Option, + #[serde(default)] + pub summary: Option, + #[serde(default)] + pub theme_tags: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAnchorItemResponse { + pub key: String, + pub label: String, + pub value: String, + pub status: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAnchorPackResponse { + pub theme_promise: PuzzleAnchorItemResponse, + pub visual_subject: PuzzleAnchorItemResponse, + pub visual_mood: PuzzleAnchorItemResponse, + pub composition_hooks: PuzzleAnchorItemResponse, + pub tags_and_forbidden: PuzzleAnchorItemResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleGeneratedImageCandidateResponse { + pub candidate_id: String, + pub image_src: String, + pub asset_id: String, + pub prompt: String, + #[serde(default)] + pub actual_prompt: Option, + pub source_type: String, + pub selected: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleCreatorIntentResponse { + pub source_mode: String, + pub raw_messages_summary: String, + pub theme_promise: String, + pub visual_subject: String, + pub visual_mood: Vec, + pub composition_hooks: Vec, + pub theme_tags: Vec, + pub forbidden_directives: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleResultDraftResponse { + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub forbidden_directives: Vec, + #[serde(default)] + pub creator_intent: Option, + pub anchor_pack: PuzzleAnchorPackResponse, + pub candidates: Vec, + #[serde(default)] + pub selected_candidate_id: Option, + #[serde(default)] + pub cover_image_src: Option, + #[serde(default)] + pub cover_asset_id: Option, + pub generation_status: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentMessageResponse { + pub id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentSuggestedActionResponse { + pub id: String, + pub action_type: String, + pub label: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleResultPreviewBlockerResponse { + pub id: String, + pub code: String, + pub message: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleResultPreviewFindingResponse { + pub id: String, + pub severity: String, + pub code: String, + pub message: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleResultPreviewEnvelopeResponse { + pub draft: PuzzleResultDraftResponse, + pub blockers: Vec, + pub quality_findings: Vec, + pub publish_ready: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentSessionSnapshotResponse { + pub session_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub anchor_pack: PuzzleAnchorPackResponse, + #[serde(default)] + pub draft: Option, + pub messages: Vec, + #[serde(default)] + pub last_assistant_reply: Option, + #[serde(default)] + pub published_profile_id: Option, + pub suggested_actions: Vec, + #[serde(default)] + pub result_preview: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentSessionResponse { + pub session: PuzzleAgentSessionSnapshotResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentOperationResponse { + pub operation_id: String, + pub operation_type: String, + pub status: String, + pub phase_label: String, + pub phase_detail: String, + pub progress: u32, + #[serde(default)] + pub error: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleAgentActionResponse { + pub operation: PuzzleAgentOperationResponse, +} diff --git a/server-rs/crates/shared-contracts/src/puzzle_gallery.rs b/server-rs/crates/shared-contracts/src/puzzle_gallery.rs new file mode 100644 index 00000000..57e73b04 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/puzzle_gallery.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::puzzle_works::PuzzleWorkSummaryResponse; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleGalleryResponse { + pub items: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleGalleryDetailResponse { + pub item: PuzzleWorkSummaryResponse, +} diff --git a/server-rs/crates/shared-contracts/src/puzzle_runtime.rs b/server-rs/crates/shared-contracts/src/puzzle_runtime.rs new file mode 100644 index 00000000..62caa0b1 --- /dev/null +++ b/server-rs/crates/shared-contracts/src/puzzle_runtime.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct StartPuzzleRunRequest { + pub profile_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SwapPuzzlePiecesRequest { + pub first_piece_id: String, + pub second_piece_id: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct DragPuzzlePieceRequest { + pub piece_id: String, + pub target_row: u32, + pub target_col: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleCellPositionResponse { + pub row: u32, + pub col: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzlePieceStateResponse { + pub piece_id: String, + pub correct_row: u32, + pub correct_col: u32, + pub current_row: u32, + pub current_col: u32, + #[serde(default)] + pub merged_group_id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleMergedGroupStateResponse { + pub group_id: String, + pub piece_ids: Vec, + pub occupied_cells: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleBoardSnapshotResponse { + pub rows: u32, + pub cols: u32, + pub pieces: Vec, + pub merged_groups: Vec, + #[serde(default)] + pub selected_piece_id: Option, + pub all_tiles_resolved: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleRuntimeLevelSnapshotResponse { + pub run_id: String, + pub level_index: u32, + pub grid_size: u32, + pub profile_id: String, + pub level_name: String, + pub author_display_name: String, + pub theme_tags: Vec, + #[serde(default)] + pub cover_image_src: Option, + pub board: PuzzleBoardSnapshotResponse, + pub status: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleRunSnapshotResponse { + pub run_id: String, + pub entry_profile_id: String, + pub cleared_level_count: u32, + pub current_level_index: u32, + pub current_grid_size: u32, + pub played_profile_ids: Vec, + pub previous_level_tags: Vec, + #[serde(default)] + pub current_level: Option, + #[serde(default)] + pub recommended_next_profile_id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleRunResponse { + pub run: PuzzleRunSnapshotResponse, +} diff --git a/server-rs/crates/shared-contracts/src/puzzle_works.rs b/server-rs/crates/shared-contracts/src/puzzle_works.rs new file mode 100644 index 00000000..8c2e4bdb --- /dev/null +++ b/server-rs/crates/shared-contracts/src/puzzle_works.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +use crate::puzzle_agent::PuzzleAnchorPackResponse; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PutPuzzleWorkRequest { + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + #[serde(default)] + pub cover_image_src: Option, + #[serde(default)] + pub cover_asset_id: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleWorkSummaryResponse { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + #[serde(default)] + pub source_session_id: Option, + pub author_display_name: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + #[serde(default)] + pub cover_image_src: Option, + #[serde(default)] + pub cover_asset_id: Option, + pub publication_status: String, + pub updated_at: String, + #[serde(default)] + pub published_at: Option, + pub play_count: u32, + pub publish_ready: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleWorkProfileResponse { + #[serde(flatten)] + pub summary: PuzzleWorkSummaryResponse, + pub anchor_pack: PuzzleAnchorPackResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleWorksResponse { + pub items: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleWorkDetailResponse { + pub item: PuzzleWorkProfileResponse, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleWorkMutationResponse { + pub item: PuzzleWorkProfileResponse, +} diff --git a/server-rs/crates/spacetime-client/Cargo.toml b/server-rs/crates/spacetime-client/Cargo.toml index 976fa6da..44ece89f 100644 --- a/server-rs/crates/spacetime-client/Cargo.toml +++ b/server-rs/crates/spacetime-client/Cargo.toml @@ -6,11 +6,13 @@ license.workspace = true [dependencies] module-ai = { path = "../module-ai" } +module-big-fish = { path = "../module-big-fish" } module-custom-world = { path = "../module-custom-world" } module-assets = { path = "../module-assets" } module-combat = { path = "../module-combat" } module-inventory = { path = "../module-inventory" } module-npc = { path = "../module-npc" } +module-puzzle = { path = "../module-puzzle" } module-runtime = { path = "../module-runtime" } module-runtime-item = { path = "../module-runtime-item" } module-story = { path = "../module-story" } diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 30104a77..717eb6db 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -46,6 +46,24 @@ use module_npc::{ NpcStanceProfile as DomainNpcStanceProfile, NpcStateSnapshot as DomainNpcStateSnapshot, ResolveNpcInteractionInput as DomainResolveNpcInteractionInput, }; +use module_puzzle::{ + PuzzleAgentMessageSnapshot as DomainPuzzleAgentMessageSnapshot, + PuzzleAgentSessionSnapshot as DomainPuzzleAgentSessionSnapshot, + PuzzleAgentSuggestedAction as DomainPuzzleAgentSuggestedAction, + PuzzleAnchorItem as DomainPuzzleAnchorItem, PuzzleAnchorPack as DomainPuzzleAnchorPack, + PuzzleBoardSnapshot as DomainPuzzleBoardSnapshot, + PuzzleCellPosition as DomainPuzzleCellPosition, + PuzzleCreatorIntent as DomainPuzzleCreatorIntent, + PuzzleGeneratedImageCandidate as DomainPuzzleGeneratedImageCandidate, + PuzzleMergedGroupState as DomainPuzzleMergedGroupState, + PuzzlePieceState as DomainPuzzlePieceState, PuzzleResultDraft as DomainPuzzleResultDraft, + PuzzleResultPreviewBlocker as DomainPuzzleResultPreviewBlocker, + PuzzleResultPreviewEnvelope as DomainPuzzleResultPreviewEnvelope, + PuzzleResultPreviewFinding as DomainPuzzleResultPreviewFinding, + PuzzleRunSnapshot as DomainPuzzleRunSnapshot, + PuzzleRuntimeLevelSnapshot as DomainPuzzleRuntimeLevelSnapshot, + PuzzleWorkProfile as DomainPuzzleWorkProfile, +}; use module_runtime::{ RuntimeBrowseHistoryRecord, RuntimeBrowseHistoryThemeMode, RuntimePlatformTheme, RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord, @@ -104,6 +122,37 @@ use crate::module_bindings::{ BattleStateProcedureResult as BindingBattleStateProcedureResult, BattleStateQueryInput as BindingBattleStateQueryInput, BattleStateSnapshot as BindingBattleStateSnapshot, BattleStatus as BindingBattleStatus, + BigFishAgentMessageKind as BindingBigFishAgentMessageKind, + BigFishAgentMessageRole as BindingBigFishAgentMessageRole, + BigFishAgentMessageSnapshot as BindingBigFishAgentMessageSnapshot, + BigFishAnchorItem as BindingBigFishAnchorItem, + BigFishAnchorPack as BindingBigFishAnchorPack, + BigFishAnchorStatus as BindingBigFishAnchorStatus, + BigFishAssetCoverage as BindingBigFishAssetCoverage, + BigFishAssetGenerateInput as BindingBigFishAssetGenerateInput, + BigFishAssetKind as BindingBigFishAssetKind, + BigFishAssetSlotSnapshot as BindingBigFishAssetSlotSnapshot, + BigFishAssetStatus as BindingBigFishAssetStatus, + BigFishBackgroundBlueprint as BindingBigFishBackgroundBlueprint, + BigFishCreationStage as BindingBigFishCreationStage, + BigFishDraftCompileInput as BindingBigFishDraftCompileInput, + BigFishGameDraft as BindingBigFishGameDraft, + BigFishLevelBlueprint as BindingBigFishLevelBlueprint, + BigFishMessageSubmitInput as BindingBigFishMessageSubmitInput, + BigFishPublishInput as BindingBigFishPublishInput, + BigFishRunGetInput as BindingBigFishRunGetInput, + BigFishRunInputSubmitInput as BindingBigFishRunInputSubmitInput, + BigFishRunProcedureResult as BindingBigFishRunProcedureResult, + BigFishRunStartInput as BindingBigFishRunStartInput, + BigFishRunStatus as BindingBigFishRunStatus, + BigFishRuntimeEntity as BindingBigFishRuntimeEntity, + BigFishRuntimeParams as BindingBigFishRuntimeParams, + BigFishRuntimeSnapshot as BindingBigFishRuntimeSnapshot, + BigFishSessionCreateInput as BindingBigFishSessionCreateInput, + BigFishSessionGetInput as BindingBigFishSessionGetInput, + BigFishSessionProcedureResult as BindingBigFishSessionProcedureResult, + BigFishSessionSnapshot as BindingBigFishSessionSnapshot, + BigFishVector2 as BindingBigFishVector2, CombatOutcome as BindingCombatOutcome, CustomWorldAgentMessageSnapshot as BindingCustomWorldAgentMessageSnapshot, CustomWorldAgentActionExecuteInput as BindingCustomWorldAgentActionExecuteInput, @@ -193,6 +242,25 @@ use crate::module_bindings::{ RuntimeSnapshotDeleteInput as BindingRuntimeSnapshotDeleteInput, RuntimeSnapshotGetInput as BindingRuntimeSnapshotGetInput, RuntimeSnapshotProcedureResult as BindingRuntimeSnapshotProcedureResult, + PuzzleAgentMessageSubmitInput as BindingPuzzleAgentMessageSubmitInput, + PuzzleAgentSessionCreateInput as BindingPuzzleAgentSessionCreateInput, + PuzzleAgentSessionGetInput as BindingPuzzleAgentSessionGetInput, + PuzzleAgentSessionProcedureResult as BindingPuzzleAgentSessionProcedureResult, + PuzzleDraftCompileInput as BindingPuzzleDraftCompileInput, + PuzzleGeneratedImagesSaveInput as BindingPuzzleGeneratedImagesSaveInput, + PuzzlePublishInput as BindingPuzzlePublishInput, + PuzzleRunDragInput as BindingPuzzleRunDragInput, + PuzzleRunGetInput as BindingPuzzleRunGetInput, + PuzzleRunNextLevelInput as BindingPuzzleRunNextLevelInput, + PuzzleRunProcedureResult as BindingPuzzleRunProcedureResult, + PuzzleRunStartInput as BindingPuzzleRunStartInput, + PuzzleRunSwapInput as BindingPuzzleRunSwapInput, + PuzzleSelectCoverImageInput as BindingPuzzleSelectCoverImageInput, + PuzzleWorkGetInput as BindingPuzzleWorkGetInput, + PuzzleWorkProcedureResult as BindingPuzzleWorkProcedureResult, + PuzzleWorkUpsertInput as BindingPuzzleWorkUpsertInput, + PuzzleWorksListInput as BindingPuzzleWorksListInput, + PuzzleWorksProcedureResult as BindingPuzzleWorksProcedureResult, RuntimeSnapshot as BindingRuntimeSnapshot, RuntimeSnapshotUpsertInput as BindingRuntimeSnapshotUpsertInput, StoryContinueInput as BindingStoryContinueInput, StoryEventKind as BindingStoryEventKind, @@ -203,22 +271,31 @@ use crate::module_bindings::{ StorySessionStateProcedureResult as BindingStorySessionStateProcedureResult, StorySessionStatus as BindingStorySessionStatus, append_ai_text_chunk_and_return_procedure::append_ai_text_chunk_and_return as _, + advance_puzzle_next_level_procedure::advance_puzzle_next_level as _, attach_ai_result_reference_and_return_procedure::attach_ai_result_reference_and_return as _, begin_story_session_and_return_procedure::begin_story_session_and_return as _, bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return as _, cancel_ai_task_and_return_procedure::cancel_ai_task_and_return as _, clear_platform_browse_history_and_return_procedure::clear_platform_browse_history_and_return as _, + compile_big_fish_draft_procedure::compile_big_fish_draft as _, + compile_puzzle_agent_draft_procedure::compile_puzzle_agent_draft as _, complete_ai_stage_and_return_procedure::complete_ai_stage_and_return as _, complete_ai_task_and_return_procedure::complete_ai_task_and_return as _, confirm_asset_object_and_return_procedure::confirm_asset_object_and_return as _, continue_story_and_return_procedure::continue_story_and_return as _, create_ai_task_and_return_procedure::create_ai_task_and_return as _, create_battle_state_and_return_procedure::create_battle_state_and_return as _, + create_big_fish_session_procedure::create_big_fish_session as _, create_custom_world_agent_session_procedure::create_custom_world_agent_session as _, + create_puzzle_agent_session_procedure::create_puzzle_agent_session as _, delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return as _, + drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group as _, execute_custom_world_agent_action_procedure::execute_custom_world_agent_action as _, fail_ai_task_and_return_procedure::fail_ai_task_and_return as _, + generate_big_fish_asset_procedure::generate_big_fish_asset as _, get_battle_state_procedure::get_battle_state as _, + get_big_fish_run_procedure::get_big_fish_run as _, + get_big_fish_session_procedure::get_big_fish_session as _, get_custom_world_agent_card_detail_procedure::get_custom_world_agent_card_detail as _, get_custom_world_agent_operation_procedure::get_custom_world_agent_operation as _, get_custom_world_agent_session_procedure::get_custom_world_agent_session as _, @@ -226,6 +303,10 @@ use crate::module_bindings::{ get_custom_world_library_detail_procedure::get_custom_world_library_detail as _, get_profile_dashboard_procedure::get_profile_dashboard as _, get_profile_play_stats_procedure::get_profile_play_stats as _, + get_puzzle_agent_session_procedure::get_puzzle_agent_session as _, + get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail as _, + get_puzzle_run_procedure::get_puzzle_run as _, + get_puzzle_work_detail_procedure::get_puzzle_work_detail as _, get_runtime_inventory_state_procedure::get_runtime_inventory_state as _, get_runtime_setting_or_default_procedure::get_runtime_setting_or_default as _, get_runtime_snapshot_procedure::get_runtime_snapshot as _, @@ -236,15 +317,28 @@ use crate::module_bindings::{ list_platform_browse_history_procedure::list_platform_browse_history as _, list_profile_save_archives_procedure::list_profile_save_archives as _, list_profile_wallet_ledger_procedure::list_profile_wallet_ledger as _, + list_puzzle_gallery_procedure::list_puzzle_gallery as _, + list_puzzle_works_procedure::list_puzzle_works as _, + publish_big_fish_game_procedure::publish_big_fish_game as _, publish_custom_world_profile_and_return_procedure::publish_custom_world_profile_and_return as _, publish_custom_world_world_procedure::publish_custom_world_world as _, + publish_puzzle_work_procedure::publish_puzzle_work as _, resolve_combat_action_and_return_procedure::resolve_combat_action_and_return as _, resolve_npc_battle_interaction_and_return_procedure::resolve_npc_battle_interaction_and_return as _, resume_profile_save_archive_and_return_procedure::resume_profile_save_archive_and_return as _, + save_puzzle_generated_images_procedure::save_puzzle_generated_images as _, + select_puzzle_cover_image_procedure::select_puzzle_cover_image as _, + start_big_fish_run_procedure::start_big_fish_run as _, + start_puzzle_run_procedure::start_puzzle_run as _, start_ai_task_reducer::start_ai_task as _, start_ai_task_stage_reducer::start_ai_task_stage as _, + submit_big_fish_input_procedure::submit_big_fish_input as _, + submit_big_fish_message_procedure::submit_big_fish_message as _, submit_custom_world_agent_message_procedure::submit_custom_world_agent_message as _, + submit_puzzle_agent_message_procedure::submit_puzzle_agent_message as _, + swap_puzzle_pieces_procedure::swap_puzzle_pieces as _, unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return as _, + update_puzzle_work_procedure::update_puzzle_work as _, upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return as _, upsert_platform_browse_history_and_return_procedure::upsert_platform_browse_history_and_return as _, upsert_runtime_setting_and_return_procedure::upsert_runtime_setting_and_return as _, @@ -846,6 +940,647 @@ impl SpacetimeClient { .await } + pub async fn create_puzzle_agent_session( + &self, + input: PuzzleAgentSessionCreateRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleAgentSessionCreateInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + seed_text: input.seed_text, + welcome_message_id: input.welcome_message_id, + welcome_message_text: input.welcome_message_text, + created_at_micros: input.created_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .create_puzzle_agent_session_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_puzzle_agent_session( + &self, + session_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = BindingPuzzleAgentSessionGetInput { + session_id, + owner_user_id, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().get_puzzle_agent_session_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn submit_puzzle_agent_message( + &self, + input: PuzzleAgentMessageSubmitRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleAgentMessageSubmitInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + user_message_id: input.user_message_id, + user_message_text: input.user_message_text, + submitted_at_micros: input.submitted_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().submit_puzzle_agent_message_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn compile_puzzle_agent_draft( + &self, + session_id: String, + owner_user_id: String, + compiled_at_micros: i64, + ) -> Result { + let procedure_input = BindingPuzzleDraftCompileInput { + session_id, + owner_user_id, + compiled_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().compile_puzzle_agent_draft_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn save_puzzle_generated_images( + &self, + input: PuzzleGeneratedImagesSaveRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleGeneratedImagesSaveInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + candidates_json: input.candidates_json, + saved_at_micros: input.saved_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().save_puzzle_generated_images_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn select_puzzle_cover_image( + &self, + input: PuzzleSelectCoverImageRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleSelectCoverImageInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + candidate_id: input.candidate_id, + selected_at_micros: input.selected_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().select_puzzle_cover_image_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_agent_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn publish_puzzle_work( + &self, + input: PuzzlePublishRecordInput, + ) -> Result { + let procedure_input = BindingPuzzlePublishInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + work_id: input.work_id, + profile_id: input.profile_id, + author_display_name: input.author_display_name, + level_name: input.level_name, + summary: input.summary, + theme_tags: input.theme_tags, + published_at_micros: input.published_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().publish_puzzle_work_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn list_puzzle_works( + &self, + owner_user_id: String, + ) -> Result, SpacetimeClientError> { + let procedure_input = BindingPuzzleWorksListInput { owner_user_id }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().list_puzzle_works_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_works_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_puzzle_work_detail( + &self, + profile_id: String, + ) -> Result { + let procedure_input = BindingPuzzleWorkGetInput { profile_id }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().get_puzzle_work_detail_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn update_puzzle_work( + &self, + input: PuzzleWorkUpsertRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleWorkUpsertInput { + profile_id: input.profile_id, + owner_user_id: input.owner_user_id, + level_name: input.level_name, + summary: input.summary, + theme_tags: input.theme_tags, + cover_image_src: input.cover_image_src, + cover_asset_id: input.cover_asset_id, + updated_at_micros: input.updated_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().update_puzzle_work_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn list_puzzle_gallery( + &self, + ) -> Result, SpacetimeClientError> { + self.call_after_connect(move |connection, sender| { + connection.procedures().list_puzzle_gallery_then(move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_works_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_puzzle_gallery_detail( + &self, + profile_id: String, + ) -> Result { + let procedure_input = BindingPuzzleWorkGetInput { profile_id }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().get_puzzle_gallery_detail_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_work_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn start_puzzle_run( + &self, + input: PuzzleRunStartRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleRunStartInput { + run_id: input.run_id, + owner_user_id: input.owner_user_id, + profile_id: input.profile_id, + started_at_micros: input.started_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().start_puzzle_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_puzzle_run( + &self, + run_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = BindingPuzzleRunGetInput { + run_id, + owner_user_id, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().get_puzzle_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn swap_puzzle_pieces( + &self, + input: PuzzleRunSwapRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleRunSwapInput { + run_id: input.run_id, + owner_user_id: input.owner_user_id, + first_piece_id: input.first_piece_id, + second_piece_id: input.second_piece_id, + swapped_at_micros: input.swapped_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().swap_puzzle_pieces_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn drag_puzzle_piece_or_group( + &self, + input: PuzzleRunDragRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleRunDragInput { + run_id: input.run_id, + owner_user_id: input.owner_user_id, + piece_id: input.piece_id, + target_row: input.target_row, + target_col: input.target_col, + dragged_at_micros: input.dragged_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().drag_puzzle_piece_or_group_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn advance_puzzle_next_level( + &self, + input: PuzzleRunNextLevelRecordInput, + ) -> Result { + let procedure_input = BindingPuzzleRunNextLevelInput { + run_id: input.run_id, + owner_user_id: input.owner_user_id, + advanced_at_micros: input.advanced_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().advance_puzzle_next_level_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_puzzle_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn create_big_fish_session( + &self, + input: BigFishSessionCreateRecordInput, + ) -> Result { + let procedure_input = BindingBigFishSessionCreateInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + seed_text: input.seed_text, + welcome_message_id: input.welcome_message_id, + welcome_message_text: input.welcome_message_text, + created_at_micros: input.created_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .create_big_fish_session_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + + pub async fn get_big_fish_session( + &self, + session_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = BindingBigFishSessionGetInput { + session_id, + owner_user_id, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().get_big_fish_session_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn submit_big_fish_message( + &self, + input: BigFishMessageSubmitRecordInput, + ) -> Result { + let procedure_input = BindingBigFishMessageSubmitInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + user_message_id: input.user_message_id, + user_message_text: input.user_message_text, + assistant_message_id: input.assistant_message_id, + submitted_at_micros: input.submitted_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().submit_big_fish_message_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn compile_big_fish_draft( + &self, + session_id: String, + owner_user_id: String, + compiled_at_micros: i64, + ) -> Result { + let procedure_input = BindingBigFishDraftCompileInput { + session_id, + owner_user_id, + compiled_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().compile_big_fish_draft_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn generate_big_fish_asset( + &self, + input: BigFishAssetGenerateRecordInput, + ) -> Result { + let procedure_input = BindingBigFishAssetGenerateInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + asset_kind: map_big_fish_asset_kind_input(input.asset_kind.as_str())?, + level: input.level, + motion_key: input.motion_key, + generated_at_micros: input.generated_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().generate_big_fish_asset_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn publish_big_fish_game( + &self, + session_id: String, + owner_user_id: String, + published_at_micros: i64, + ) -> Result { + let procedure_input = BindingBigFishPublishInput { + session_id, + owner_user_id, + published_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().publish_big_fish_game_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_session_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn start_big_fish_run( + &self, + input: BigFishRunStartRecordInput, + ) -> Result { + let procedure_input = BindingBigFishRunStartInput { + run_id: input.run_id, + session_id: input.session_id, + owner_user_id: input.owner_user_id, + started_at_micros: input.started_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().start_big_fish_run_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn submit_big_fish_input( + &self, + input: BigFishRunInputSubmitRecordInput, + ) -> Result { + let procedure_input = BindingBigFishRunInputSubmitInput { + run_id: input.run_id, + owner_user_id: input.owner_user_id, + input_x: input.input_x, + input_y: input.input_y, + submitted_at_micros: input.submitted_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection.procedures().submit_big_fish_input_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_run_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + + pub async fn get_big_fish_run( + &self, + run_id: String, + owner_user_id: String, + ) -> Result { + let procedure_input = BindingBigFishRunGetInput { + run_id, + owner_user_id, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .get_big_fish_run_then(procedure_input, move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_big_fish_run_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + pub async fn submit_custom_world_agent_message( &self, input: CustomWorldAgentMessageSubmitRecordInput, @@ -2401,6 +3136,138 @@ fn map_custom_world_agent_action_execute_result( }) } +fn map_puzzle_agent_session_procedure_result( + result: BindingPuzzleAgentSessionProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let session_json = result.session_json.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 puzzle agent session 快照".to_string(), + ) + })?; + let session: DomainPuzzleAgentSessionSnapshot = + serde_json::from_str(&session_json).map_err(|error| { + SpacetimeClientError::Runtime(format!( + "puzzle agent session_json 非法: {error}" + )) + })?; + Ok(map_puzzle_agent_session_snapshot(session)) +} + +fn map_puzzle_work_procedure_result( + result: BindingPuzzleWorkProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let item_json = result.item_json.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 puzzle work 快照".to_string(), + ) + })?; + let item: DomainPuzzleWorkProfile = serde_json::from_str(&item_json).map_err(|error| { + SpacetimeClientError::Runtime(format!("puzzle work item_json 非法: {error}")) + })?; + Ok(map_puzzle_work_profile(item)) +} + +fn map_puzzle_works_procedure_result( + result: BindingPuzzleWorksProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let items_json = result.items_json.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 puzzle works 快照".to_string(), + ) + })?; + let items: Vec = + serde_json::from_str(&items_json).map_err(|error| { + SpacetimeClientError::Runtime(format!("puzzle works items_json 非法: {error}")) + })?; + Ok(items.into_iter().map(map_puzzle_work_profile).collect()) +} + +fn map_puzzle_run_procedure_result( + result: BindingPuzzleRunProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let run_json = result.run_json.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 puzzle run 快照".to_string(), + ) + })?; + let run: DomainPuzzleRunSnapshot = serde_json::from_str(&run_json).map_err(|error| { + SpacetimeClientError::Runtime(format!("puzzle run run_json 非法: {error}")) + })?; + Ok(map_puzzle_run_snapshot(run)) +} + +fn map_big_fish_session_procedure_result( + result: BindingBigFishSessionProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let session = result.session.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 big fish session 快照".to_string(), + ) + })?; + + Ok(map_big_fish_session_snapshot(session)) +} + +fn map_big_fish_run_procedure_result( + result: BindingBigFishRunProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::Procedure( + result + .error_message + .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), + )); + } + + let run = result.run.ok_or_else(|| { + SpacetimeClientError::Procedure( + "SpacetimeDB procedure 未返回 big fish runtime 快照".to_string(), + ) + })?; + + Ok(map_big_fish_runtime_snapshot(run)) +} + fn map_story_session_procedure_result( result: BindingStorySessionProcedureResult, ) -> Result { @@ -3026,6 +3893,455 @@ fn map_custom_world_draft_card_detail_section_snapshot( } } +fn map_big_fish_session_snapshot(snapshot: BindingBigFishSessionSnapshot) -> BigFishSessionRecord { + BigFishSessionRecord { + session_id: snapshot.session_id, + current_turn: snapshot.current_turn, + progress_percent: snapshot.progress_percent, + stage: format_big_fish_creation_stage(snapshot.stage).to_string(), + anchor_pack: map_big_fish_anchor_pack(snapshot.anchor_pack), + draft: snapshot.draft.map(map_big_fish_game_draft), + asset_slots: snapshot + .asset_slots + .into_iter() + .map(map_big_fish_asset_slot_snapshot) + .collect(), + asset_coverage: map_big_fish_asset_coverage(snapshot.asset_coverage), + messages: snapshot + .messages + .into_iter() + .map(map_big_fish_agent_message_snapshot) + .collect(), + last_assistant_reply: snapshot.last_assistant_reply, + publish_ready: snapshot.publish_ready, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_puzzle_agent_session_snapshot( + snapshot: DomainPuzzleAgentSessionSnapshot, +) -> PuzzleAgentSessionRecord { + PuzzleAgentSessionRecord { + session_id: snapshot.session_id, + current_turn: snapshot.current_turn, + progress_percent: snapshot.progress_percent, + stage: snapshot.stage.as_str().to_string(), + anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack), + draft: snapshot.draft.map(map_puzzle_result_draft), + messages: snapshot + .messages + .into_iter() + .map(map_puzzle_agent_message_snapshot) + .collect(), + last_assistant_reply: snapshot.last_assistant_reply, + published_profile_id: snapshot.published_profile_id, + suggested_actions: snapshot + .suggested_actions + .into_iter() + .map(map_puzzle_suggested_action) + .collect(), + result_preview: snapshot.result_preview.map(map_puzzle_result_preview), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_puzzle_anchor_pack(snapshot: DomainPuzzleAnchorPack) -> PuzzleAnchorPackRecord { + PuzzleAnchorPackRecord { + theme_promise: map_puzzle_anchor_item(snapshot.theme_promise), + visual_subject: map_puzzle_anchor_item(snapshot.visual_subject), + visual_mood: map_puzzle_anchor_item(snapshot.visual_mood), + composition_hooks: map_puzzle_anchor_item(snapshot.composition_hooks), + tags_and_forbidden: map_puzzle_anchor_item(snapshot.tags_and_forbidden), + } +} + +fn map_puzzle_anchor_item(snapshot: DomainPuzzleAnchorItem) -> PuzzleAnchorItemRecord { + PuzzleAnchorItemRecord { + key: snapshot.key, + label: snapshot.label, + value: snapshot.value, + status: snapshot.status.as_str().to_string(), + } +} + +fn map_puzzle_result_draft(snapshot: DomainPuzzleResultDraft) -> PuzzleResultDraftRecord { + PuzzleResultDraftRecord { + level_name: snapshot.level_name, + summary: snapshot.summary, + theme_tags: snapshot.theme_tags, + forbidden_directives: snapshot.forbidden_directives, + creator_intent: snapshot.creator_intent.map(map_puzzle_creator_intent), + anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack), + candidates: snapshot + .candidates + .into_iter() + .map(map_puzzle_generated_image_candidate) + .collect(), + selected_candidate_id: snapshot.selected_candidate_id, + cover_image_src: snapshot.cover_image_src, + cover_asset_id: snapshot.cover_asset_id, + generation_status: snapshot.generation_status, + } +} + +fn map_puzzle_creator_intent(snapshot: DomainPuzzleCreatorIntent) -> PuzzleCreatorIntentRecord { + PuzzleCreatorIntentRecord { + source_mode: snapshot.source_mode, + raw_messages_summary: snapshot.raw_messages_summary, + theme_promise: snapshot.theme_promise, + visual_subject: snapshot.visual_subject, + visual_mood: snapshot.visual_mood, + composition_hooks: snapshot.composition_hooks, + theme_tags: snapshot.theme_tags, + forbidden_directives: snapshot.forbidden_directives, + } +} + +fn map_puzzle_generated_image_candidate( + snapshot: DomainPuzzleGeneratedImageCandidate, +) -> PuzzleGeneratedImageCandidateRecord { + PuzzleGeneratedImageCandidateRecord { + candidate_id: snapshot.candidate_id, + image_src: snapshot.image_src, + asset_id: snapshot.asset_id, + prompt: snapshot.prompt, + actual_prompt: snapshot.actual_prompt, + source_type: snapshot.source_type, + selected: snapshot.selected, + } +} + +fn map_puzzle_agent_message_snapshot( + snapshot: DomainPuzzleAgentMessageSnapshot, +) -> PuzzleAgentMessageRecord { + PuzzleAgentMessageRecord { + message_id: snapshot.message_id, + role: snapshot.role.as_str().to_string(), + kind: snapshot.kind.as_str().to_string(), + text: snapshot.text, + created_at: format_timestamp_micros(snapshot.created_at_micros), + } +} + +fn map_puzzle_suggested_action( + snapshot: DomainPuzzleAgentSuggestedAction, +) -> PuzzleAgentSuggestedActionRecord { + PuzzleAgentSuggestedActionRecord { + action_id: snapshot.id, + action_type: snapshot.action_type, + label: snapshot.label, + } +} + +fn map_puzzle_result_preview( + snapshot: DomainPuzzleResultPreviewEnvelope, +) -> PuzzleResultPreviewRecord { + PuzzleResultPreviewRecord { + draft: map_puzzle_result_draft(snapshot.draft), + blockers: snapshot + .blockers + .into_iter() + .map(map_puzzle_result_preview_blocker) + .collect(), + quality_findings: snapshot + .quality_findings + .into_iter() + .map(map_puzzle_result_preview_finding) + .collect(), + publish_ready: snapshot.publish_ready, + } +} + +fn map_puzzle_result_preview_blocker( + snapshot: DomainPuzzleResultPreviewBlocker, +) -> PuzzleResultPreviewBlockerRecord { + PuzzleResultPreviewBlockerRecord { + blocker_id: snapshot.id, + code: snapshot.code, + message: snapshot.message, + } +} + +fn map_puzzle_result_preview_finding( + snapshot: DomainPuzzleResultPreviewFinding, +) -> PuzzleResultPreviewFindingRecord { + PuzzleResultPreviewFindingRecord { + finding_id: snapshot.id, + severity: snapshot.severity, + code: snapshot.code, + message: snapshot.message, + } +} + +fn map_puzzle_work_profile(snapshot: DomainPuzzleWorkProfile) -> PuzzleWorkProfileRecord { + PuzzleWorkProfileRecord { + work_id: snapshot.work_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + source_session_id: snapshot.source_session_id, + author_display_name: snapshot.author_display_name, + level_name: snapshot.level_name, + summary: snapshot.summary, + theme_tags: snapshot.theme_tags, + cover_image_src: snapshot.cover_image_src, + cover_asset_id: snapshot.cover_asset_id, + publication_status: snapshot.publication_status.as_str().to_string(), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + published_at: snapshot.published_at_micros.map(format_timestamp_micros), + play_count: snapshot.play_count, + publish_ready: snapshot.publish_ready, + anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack), + } +} + +fn map_puzzle_run_snapshot(snapshot: DomainPuzzleRunSnapshot) -> PuzzleRunRecord { + PuzzleRunRecord { + run_id: snapshot.run_id, + entry_profile_id: snapshot.entry_profile_id, + cleared_level_count: snapshot.cleared_level_count, + current_level_index: snapshot.current_level_index, + current_grid_size: snapshot.current_grid_size, + played_profile_ids: snapshot.played_profile_ids, + previous_level_tags: snapshot.previous_level_tags, + current_level: snapshot.current_level.map(map_puzzle_runtime_level_snapshot), + recommended_next_profile_id: snapshot.recommended_next_profile_id, + } +} + +fn map_puzzle_runtime_level_snapshot( + snapshot: DomainPuzzleRuntimeLevelSnapshot, +) -> PuzzleRuntimeLevelRecord { + PuzzleRuntimeLevelRecord { + run_id: snapshot.run_id, + level_index: snapshot.level_index, + grid_size: snapshot.grid_size, + profile_id: snapshot.profile_id, + level_name: snapshot.level_name, + author_display_name: snapshot.author_display_name, + theme_tags: snapshot.theme_tags, + cover_image_src: snapshot.cover_image_src, + board: map_puzzle_board_snapshot(snapshot.board), + status: snapshot.status.as_str().to_string(), + } +} + +fn map_puzzle_board_snapshot(snapshot: DomainPuzzleBoardSnapshot) -> PuzzleBoardRecord { + PuzzleBoardRecord { + rows: snapshot.rows, + cols: snapshot.cols, + pieces: snapshot + .pieces + .into_iter() + .map(map_puzzle_piece_state) + .collect(), + merged_groups: snapshot + .merged_groups + .into_iter() + .map(map_puzzle_merged_group_state) + .collect(), + selected_piece_id: snapshot.selected_piece_id, + all_tiles_resolved: snapshot.all_tiles_resolved, + } +} + +fn map_puzzle_piece_state(snapshot: DomainPuzzlePieceState) -> PuzzlePieceStateRecord { + PuzzlePieceStateRecord { + piece_id: snapshot.piece_id, + correct_row: snapshot.correct_row, + correct_col: snapshot.correct_col, + current_row: snapshot.current_row, + current_col: snapshot.current_col, + merged_group_id: snapshot.merged_group_id, + } +} + +fn map_puzzle_merged_group_state( + snapshot: DomainPuzzleMergedGroupState, +) -> PuzzleMergedGroupRecord { + PuzzleMergedGroupRecord { + group_id: snapshot.group_id, + piece_ids: snapshot.piece_ids, + occupied_cells: snapshot + .occupied_cells + .into_iter() + .map(map_puzzle_cell_position) + .collect(), + } +} + +fn map_puzzle_cell_position(snapshot: DomainPuzzleCellPosition) -> PuzzleCellPositionRecord { + PuzzleCellPositionRecord { + row: snapshot.row, + col: snapshot.col, + } +} + +fn map_big_fish_anchor_pack(snapshot: BindingBigFishAnchorPack) -> BigFishAnchorPackRecord { + BigFishAnchorPackRecord { + gameplay_promise: map_big_fish_anchor_item(snapshot.gameplay_promise), + ecology_visual_theme: map_big_fish_anchor_item(snapshot.ecology_visual_theme), + growth_ladder: map_big_fish_anchor_item(snapshot.growth_ladder), + risk_tempo: map_big_fish_anchor_item(snapshot.risk_tempo), + } +} + +fn map_big_fish_anchor_item(snapshot: BindingBigFishAnchorItem) -> BigFishAnchorItemRecord { + BigFishAnchorItemRecord { + key: snapshot.key, + label: snapshot.label, + value: snapshot.value, + status: format_big_fish_anchor_status(snapshot.status).to_string(), + } +} + +fn map_big_fish_game_draft(snapshot: BindingBigFishGameDraft) -> BigFishGameDraftRecord { + BigFishGameDraftRecord { + title: snapshot.title, + subtitle: snapshot.subtitle, + core_fun: snapshot.core_fun, + ecology_theme: snapshot.ecology_theme, + levels: snapshot + .levels + .into_iter() + .map(map_big_fish_level_blueprint) + .collect(), + background: map_big_fish_background_blueprint(snapshot.background), + runtime_params: map_big_fish_runtime_params(snapshot.runtime_params), + } +} + +fn map_big_fish_level_blueprint( + snapshot: BindingBigFishLevelBlueprint, +) -> BigFishLevelBlueprintRecord { + BigFishLevelBlueprintRecord { + level: snapshot.level, + name: snapshot.name, + one_line_fantasy: snapshot.one_line_fantasy, + silhouette_direction: snapshot.silhouette_direction, + size_ratio: snapshot.size_ratio, + visual_prompt_seed: snapshot.visual_prompt_seed, + motion_prompt_seed: snapshot.motion_prompt_seed, + merge_source_level: snapshot.merge_source_level, + prey_window: snapshot.prey_window, + threat_window: snapshot.threat_window, + is_final_level: snapshot.is_final_level, + } +} + +fn map_big_fish_background_blueprint( + snapshot: BindingBigFishBackgroundBlueprint, +) -> BigFishBackgroundBlueprintRecord { + BigFishBackgroundBlueprintRecord { + theme: snapshot.theme, + color_mood: snapshot.color_mood, + foreground_hints: snapshot.foreground_hints, + midground_composition: snapshot.midground_composition, + background_depth: snapshot.background_depth, + safe_play_area_hint: snapshot.safe_play_area_hint, + spawn_edge_hint: snapshot.spawn_edge_hint, + background_prompt_seed: snapshot.background_prompt_seed, + } +} + +fn map_big_fish_runtime_params(snapshot: BindingBigFishRuntimeParams) -> BigFishRuntimeParamsRecord { + BigFishRuntimeParamsRecord { + level_count: snapshot.level_count, + merge_count_per_upgrade: snapshot.merge_count_per_upgrade, + spawn_target_count: snapshot.spawn_target_count, + leader_move_speed: snapshot.leader_move_speed, + follower_catch_up_speed: snapshot.follower_catch_up_speed, + offscreen_cull_seconds: snapshot.offscreen_cull_seconds, + prey_spawn_delta_levels: snapshot.prey_spawn_delta_levels, + threat_spawn_delta_levels: snapshot.threat_spawn_delta_levels, + win_level: snapshot.win_level, + } +} + +fn map_big_fish_asset_slot_snapshot( + snapshot: BindingBigFishAssetSlotSnapshot, +) -> BigFishAssetSlotRecord { + BigFishAssetSlotRecord { + slot_id: snapshot.slot_id, + asset_kind: format_big_fish_asset_kind(snapshot.asset_kind).to_string(), + level: snapshot.level, + motion_key: snapshot.motion_key, + status: format_big_fish_asset_status(snapshot.status).to_string(), + asset_url: snapshot.asset_url, + prompt_snapshot: snapshot.prompt_snapshot, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_big_fish_asset_coverage( + snapshot: BindingBigFishAssetCoverage, +) -> BigFishAssetCoverageRecord { + BigFishAssetCoverageRecord { + level_main_image_ready_count: snapshot.level_main_image_ready_count, + level_motion_ready_count: snapshot.level_motion_ready_count, + background_ready: snapshot.background_ready, + required_level_count: snapshot.required_level_count, + publish_ready: snapshot.publish_ready, + blockers: snapshot.blockers, + } +} + +fn map_big_fish_agent_message_snapshot( + snapshot: BindingBigFishAgentMessageSnapshot, +) -> BigFishAgentMessageRecord { + BigFishAgentMessageRecord { + message_id: snapshot.message_id, + role: format_big_fish_agent_message_role(snapshot.role).to_string(), + kind: format_big_fish_agent_message_kind(snapshot.kind).to_string(), + text: snapshot.text, + created_at: format_timestamp_micros(snapshot.created_at_micros), + } +} + +fn map_big_fish_runtime_snapshot(snapshot: BindingBigFishRuntimeSnapshot) -> BigFishRuntimeRecord { + BigFishRuntimeRecord { + run_id: snapshot.run_id, + session_id: snapshot.session_id, + status: format_big_fish_run_status(snapshot.status).to_string(), + tick: snapshot.tick, + player_level: snapshot.player_level, + win_level: snapshot.win_level, + leader_entity_id: snapshot.leader_entity_id, + owned_entities: snapshot + .owned_entities + .into_iter() + .map(map_big_fish_runtime_entity) + .collect(), + wild_entities: snapshot + .wild_entities + .into_iter() + .map(map_big_fish_runtime_entity) + .collect(), + camera_center: map_big_fish_vector2(snapshot.camera_center), + last_input: map_big_fish_vector2(snapshot.last_input), + event_log: snapshot.event_log, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + +fn map_big_fish_runtime_entity( + snapshot: BindingBigFishRuntimeEntity, +) -> BigFishRuntimeEntityRecord { + BigFishRuntimeEntityRecord { + entity_id: snapshot.entity_id, + level: snapshot.level, + position: map_big_fish_vector2(snapshot.position), + radius: snapshot.radius, + offscreen_seconds: snapshot.offscreen_seconds, + } +} + +fn map_big_fish_vector2(snapshot: BindingBigFishVector2) -> BigFishVector2Record { + BigFishVector2Record { + x: snapshot.x, + y: snapshot.y, + } +} + fn map_story_session_snapshot(snapshot: BindingStorySessionSnapshot) -> StorySessionRecord { StorySessionRecord { story_session_id: snapshot.story_session_id, @@ -3565,6 +4881,78 @@ fn format_custom_world_role_asset_status_back( .to_string() } +fn map_big_fish_asset_kind_input( + value: &str, +) -> Result { + match value.trim() { + "level_main_image" => Ok(BindingBigFishAssetKind::LevelMainImage), + "level_motion" => Ok(BindingBigFishAssetKind::LevelMotion), + "stage_background" => Ok(BindingBigFishAssetKind::StageBackground), + other => Err(SpacetimeClientError::Runtime(format!( + "big fish asset kind `{other}` 当前尚未支持" + ))), + } +} + +fn format_big_fish_creation_stage(value: BindingBigFishCreationStage) -> &'static str { + match value { + BindingBigFishCreationStage::CollectingAnchors => "collecting_anchors", + BindingBigFishCreationStage::DraftReady => "draft_ready", + BindingBigFishCreationStage::AssetRefining => "asset_refining", + BindingBigFishCreationStage::ReadyToPublish => "ready_to_publish", + BindingBigFishCreationStage::Published => "published", + } +} + +fn format_big_fish_anchor_status(value: BindingBigFishAnchorStatus) -> &'static str { + match value { + BindingBigFishAnchorStatus::Confirmed => "confirmed", + BindingBigFishAnchorStatus::Inferred => "inferred", + BindingBigFishAnchorStatus::Missing => "missing", + BindingBigFishAnchorStatus::Locked => "locked", + } +} + +fn format_big_fish_agent_message_role(value: BindingBigFishAgentMessageRole) -> &'static str { + match value { + BindingBigFishAgentMessageRole::User => "user", + BindingBigFishAgentMessageRole::Assistant => "assistant", + BindingBigFishAgentMessageRole::System => "system", + } +} + +fn format_big_fish_agent_message_kind(value: BindingBigFishAgentMessageKind) -> &'static str { + match value { + BindingBigFishAgentMessageKind::Chat => "chat", + BindingBigFishAgentMessageKind::Summary => "summary", + BindingBigFishAgentMessageKind::ActionResult => "action_result", + BindingBigFishAgentMessageKind::Warning => "warning", + } +} + +fn format_big_fish_asset_kind(value: BindingBigFishAssetKind) -> &'static str { + match value { + BindingBigFishAssetKind::LevelMainImage => "level_main_image", + BindingBigFishAssetKind::LevelMotion => "level_motion", + BindingBigFishAssetKind::StageBackground => "stage_background", + } +} + +fn format_big_fish_asset_status(value: BindingBigFishAssetStatus) -> &'static str { + match value { + BindingBigFishAssetStatus::Missing => "missing", + BindingBigFishAssetStatus::Ready => "ready", + } +} + +fn format_big_fish_run_status(value: BindingBigFishRunStatus) -> &'static str { + match value { + BindingBigFishRunStatus::Running => "running", + BindingBigFishRunStatus::Won => "won", + BindingBigFishRunStatus::Failed => "failed", + } +} + fn format_custom_world_theme_mode(value: DomainCustomWorldThemeMode) -> &'static str { match value { DomainCustomWorldThemeMode::Martial => "martial", @@ -4315,6 +5703,483 @@ pub struct CustomWorldAgentActionExecuteRecord { pub operation: CustomWorldAgentOperationRecord, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAgentSessionCreateRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAgentMessageSubmitRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub submitted_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleGeneratedImagesSaveRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub candidates_json: String, + pub saved_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleSelectCoverImageRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub candidate_id: String, + pub selected_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzlePublishRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub work_id: String, + pub profile_id: String, + pub author_display_name: String, + pub level_name: Option, + pub summary: Option, + pub theme_tags: Option>, + pub published_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleWorkUpsertRecordInput { + pub profile_id: String, + pub owner_user_id: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRunStartRecordInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub started_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRunSwapRecordInput { + pub run_id: String, + pub owner_user_id: String, + pub first_piece_id: String, + pub second_piece_id: String, + pub swapped_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRunDragRecordInput { + pub run_id: String, + pub owner_user_id: String, + pub piece_id: String, + pub target_row: u32, + pub target_col: u32, + pub dragged_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRunNextLevelRecordInput { + pub run_id: String, + pub owner_user_id: String, + pub advanced_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAnchorItemRecord { + pub key: String, + pub label: String, + pub value: String, + pub status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAnchorPackRecord { + pub theme_promise: PuzzleAnchorItemRecord, + pub visual_subject: PuzzleAnchorItemRecord, + pub visual_mood: PuzzleAnchorItemRecord, + pub composition_hooks: PuzzleAnchorItemRecord, + pub tags_and_forbidden: PuzzleAnchorItemRecord, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleCreatorIntentRecord { + pub source_mode: String, + pub raw_messages_summary: String, + pub theme_promise: String, + pub visual_subject: String, + pub visual_mood: Vec, + pub composition_hooks: Vec, + pub theme_tags: Vec, + pub forbidden_directives: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleGeneratedImageCandidateRecord { + pub candidate_id: String, + pub image_src: String, + pub asset_id: String, + pub prompt: String, + pub actual_prompt: Option, + pub source_type: String, + pub selected: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleResultDraftRecord { + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub forbidden_directives: Vec, + pub creator_intent: Option, + pub anchor_pack: PuzzleAnchorPackRecord, + pub candidates: Vec, + pub selected_candidate_id: Option, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub generation_status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAgentMessageRecord { + pub message_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAgentSuggestedActionRecord { + pub action_id: String, + pub action_type: String, + pub label: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleResultPreviewBlockerRecord { + pub blocker_id: String, + pub code: String, + pub message: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleResultPreviewFindingRecord { + pub finding_id: String, + pub severity: String, + pub code: String, + pub message: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleResultPreviewRecord { + pub draft: PuzzleResultDraftRecord, + pub blockers: Vec, + pub quality_findings: Vec, + pub publish_ready: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleAgentSessionRecord { + pub session_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub anchor_pack: PuzzleAnchorPackRecord, + pub draft: Option, + pub messages: Vec, + pub last_assistant_reply: Option, + pub published_profile_id: Option, + pub suggested_actions: Vec, + pub result_preview: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleWorkProfileRecord { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub publication_status: String, + pub updated_at: String, + pub published_at: Option, + pub play_count: u32, + pub publish_ready: bool, + pub anchor_pack: PuzzleAnchorPackRecord, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleCellPositionRecord { + pub row: u32, + pub col: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzlePieceStateRecord { + pub piece_id: String, + pub correct_row: u32, + pub correct_col: u32, + pub current_row: u32, + pub current_col: u32, + pub merged_group_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleMergedGroupRecord { + pub group_id: String, + pub piece_ids: Vec, + pub occupied_cells: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleBoardRecord { + pub rows: u32, + pub cols: u32, + pub pieces: Vec, + pub merged_groups: Vec, + pub selected_piece_id: Option, + pub all_tiles_resolved: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRuntimeLevelRecord { + pub run_id: String, + pub level_index: u32, + pub grid_size: u32, + pub profile_id: String, + pub level_name: String, + pub author_display_name: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub board: PuzzleBoardRecord, + pub status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleRunRecord { + pub run_id: String, + pub entry_profile_id: String, + pub cleared_level_count: u32, + pub current_level_index: u32, + pub current_grid_size: u32, + pub played_profile_ids: Vec, + pub previous_level_tags: Vec, + pub current_level: Option, + pub recommended_next_profile_id: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishSessionCreateRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishMessageSubmitRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub assistant_message_id: String, + pub submitted_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAssetGenerateRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub asset_kind: String, + pub level: Option, + pub motion_key: Option, + pub generated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishRunStartRecordInput { + pub run_id: String, + pub session_id: String, + pub owner_user_id: String, + pub started_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishRunInputSubmitRecordInput { + pub run_id: String, + pub owner_user_id: String, + pub input_x: f32, + pub input_y: f32, + pub submitted_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAnchorItemRecord { + pub key: String, + pub label: String, + pub value: String, + pub status: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAnchorPackRecord { + pub gameplay_promise: BigFishAnchorItemRecord, + pub ecology_visual_theme: BigFishAnchorItemRecord, + pub growth_ladder: BigFishAnchorItemRecord, + pub risk_tempo: BigFishAnchorItemRecord, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishLevelBlueprintRecord { + pub level: u32, + pub name: String, + pub one_line_fantasy: String, + pub silhouette_direction: String, + pub size_ratio: f32, + pub visual_prompt_seed: String, + pub motion_prompt_seed: String, + pub merge_source_level: Option, + pub prey_window: Vec, + pub threat_window: Vec, + pub is_final_level: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishBackgroundBlueprintRecord { + pub theme: String, + pub color_mood: String, + pub foreground_hints: String, + pub midground_composition: String, + pub background_depth: String, + pub safe_play_area_hint: String, + pub spawn_edge_hint: String, + pub background_prompt_seed: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishRuntimeParamsRecord { + pub level_count: u32, + pub merge_count_per_upgrade: u32, + pub spawn_target_count: u32, + pub leader_move_speed: f32, + pub follower_catch_up_speed: f32, + pub offscreen_cull_seconds: f32, + pub prey_spawn_delta_levels: Vec, + pub threat_spawn_delta_levels: Vec, + pub win_level: u32, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishGameDraftRecord { + pub title: String, + pub subtitle: String, + pub core_fun: String, + pub ecology_theme: String, + pub levels: Vec, + pub background: BigFishBackgroundBlueprintRecord, + pub runtime_params: BigFishRuntimeParamsRecord, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAgentMessageRecord { + pub message_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAssetSlotRecord { + pub slot_id: String, + pub asset_kind: String, + pub level: Option, + pub motion_key: Option, + pub status: String, + pub asset_url: Option, + pub prompt_snapshot: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BigFishAssetCoverageRecord { + pub level_main_image_ready_count: u32, + pub level_motion_ready_count: u32, + pub background_ready: bool, + pub required_level_count: u32, + pub publish_ready: bool, + pub blockers: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishSessionRecord { + pub session_id: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub anchor_pack: BigFishAnchorPackRecord, + pub draft: Option, + pub asset_slots: Vec, + pub asset_coverage: BigFishAssetCoverageRecord, + pub messages: Vec, + pub last_assistant_reply: Option, + pub publish_ready: bool, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishVector2Record { + pub x: f32, + pub y: f32, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishRuntimeEntityRecord { + pub entity_id: String, + pub level: u32, + pub position: BigFishVector2Record, + pub radius: f32, + pub offscreen_seconds: f32, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BigFishRuntimeRecord { + pub run_id: String, + pub session_id: String, + pub status: String, + pub tick: u64, + pub player_level: u32, + pub win_level: u32, + pub leader_entity_id: Option, + pub owned_entities: Vec, + pub wild_entities: Vec, + pub camera_center: BigFishVector2Record, + pub last_input: BigFishVector2Record, + pub event_log: Vec, + pub updated_at: String, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResolveNpcBattleInteractionInput { pub npc_interaction: DomainResolveNpcInteractionInput, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs new file mode 100644 index 00000000..4a7804c7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_run_next_level_input_type::PuzzleRunNextLevelInput; +use super::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct AdvancePuzzleNextLevelArgs { + pub input: PuzzleRunNextLevelInput, +} + + +impl __sdk::InModule for AdvancePuzzleNextLevelArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `advance_puzzle_next_level`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait advance_puzzle_next_level { + fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput, +) { + self.advance_puzzle_next_level_then(input, |_, _| {}); + } + + fn advance_puzzle_next_level_then( + &self, + input: PuzzleRunNextLevelInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl advance_puzzle_next_level for super::RemoteProcedures { + fn advance_puzzle_next_level_then( + &self, + input: PuzzleRunNextLevelInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( + "advance_puzzle_next_level", + AdvancePuzzleNextLevelArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_kind_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_kind_type.rs new file mode 100644 index 00000000..174e55f8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_kind_type.rs @@ -0,0 +1,31 @@ +// 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 BigFishAgentMessageKind { + Chat, + + Summary, + + ActionResult, + + Warning, + +} + + + +impl __sdk::InModule for BigFishAgentMessageKind { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_role_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_role_type.rs new file mode 100644 index 00000000..9f8a0458 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_role_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, +}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum BigFishAgentMessageRole { + User, + + Assistant, + + System, + +} + + + +impl __sdk::InModule for BigFishAgentMessageRole { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_snapshot_type.rs new file mode 100644 index 00000000..d6b00fc5 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_snapshot_type.rs @@ -0,0 +1,30 @@ +// 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::big_fish_agent_message_role_type::BigFishAgentMessageRole; +use super::big_fish_agent_message_kind_type::BigFishAgentMessageKind; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: BigFishAgentMessageRole, + pub kind: BigFishAgentMessageKind, + pub text: String, + pub created_at_micros: i64, +} + + +impl __sdk::InModule for BigFishAgentMessageSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_table.rs new file mode 100644 index 00000000..59185a0b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_table.rs @@ -0,0 +1,165 @@ +// 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::big_fish_agent_message_type::BigFishAgentMessage; +use super::big_fish_agent_message_role_type::BigFishAgentMessageRole; +use super::big_fish_agent_message_kind_type::BigFishAgentMessageKind; + +/// Table handle for the table `big_fish_agent_message`. +/// +/// Obtain a handle from the [`BigFishAgentMessageTableAccess::big_fish_agent_message`] method on [`super::RemoteTables`], +/// like `ctx.db.big_fish_agent_message()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.big_fish_agent_message().on_insert(...)`. +pub struct BigFishAgentMessageTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `big_fish_agent_message`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BigFishAgentMessageTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BigFishAgentMessageTableHandle`], which mediates access to the table `big_fish_agent_message`. + fn big_fish_agent_message(&self) -> BigFishAgentMessageTableHandle<'_>; +} + +impl BigFishAgentMessageTableAccess for super::RemoteTables { + fn big_fish_agent_message(&self) -> BigFishAgentMessageTableHandle<'_> { + BigFishAgentMessageTableHandle { + imp: self.imp.get_table::("big_fish_agent_message"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BigFishAgentMessageInsertCallbackId(__sdk::CallbackId); +pub struct BigFishAgentMessageDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BigFishAgentMessageTableHandle<'ctx> { + type Row = BigFishAgentMessage; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = BigFishAgentMessageInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishAgentMessageInsertCallbackId { + BigFishAgentMessageInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BigFishAgentMessageInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BigFishAgentMessageDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishAgentMessageDeleteCallbackId { + BigFishAgentMessageDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BigFishAgentMessageDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BigFishAgentMessageUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BigFishAgentMessageTableHandle<'ctx> { + type UpdateCallbackId = BigFishAgentMessageUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BigFishAgentMessageUpdateCallbackId { + BigFishAgentMessageUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BigFishAgentMessageUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `message_id` unique index on the table `big_fish_agent_message`, + /// which allows point queries on the field of the same name + /// via the [`BigFishAgentMessageMessageIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.big_fish_agent_message().message_id().find(...)`. + pub struct BigFishAgentMessageMessageIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> BigFishAgentMessageTableHandle<'ctx> { + /// Get a handle on the `message_id` unique index on the table `big_fish_agent_message`. + pub fn message_id(&self) -> BigFishAgentMessageMessageIdUnique<'ctx> { + BigFishAgentMessageMessageIdUnique { + imp: self.imp.get_unique_constraint::("message_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> BigFishAgentMessageMessageIdUnique<'ctx> { + /// Find the subscribed row whose `message_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::("big_fish_agent_message"); + _table.add_unique_constraint::("message_id", |row| &row.message_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 `BigFishAgentMessage`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait big_fish_agent_messageQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BigFishAgentMessage`. + fn big_fish_agent_message(&self) -> __sdk::__query_builder::Table; + } + + impl big_fish_agent_messageQueryTableAccess for __sdk::QueryTableAccessor { + fn big_fish_agent_message(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("big_fish_agent_message") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_type.rs new file mode 100644 index 00000000..c55ecc3d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_agent_message_type.rs @@ -0,0 +1,79 @@ +// 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::big_fish_agent_message_role_type::BigFishAgentMessageRole; +use super::big_fish_agent_message_kind_type::BigFishAgentMessageKind; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAgentMessage { + pub message_id: String, + pub session_id: String, + pub role: BigFishAgentMessageRole, + pub kind: BigFishAgentMessageKind, + pub text: String, + pub created_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for BigFishAgentMessage { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `BigFishAgentMessage`. +/// +/// Provides typed access to columns for query building. +pub struct BigFishAgentMessageCols { + pub message_id: __sdk::__query_builder::Col, + pub session_id: __sdk::__query_builder::Col, + pub role: __sdk::__query_builder::Col, + pub kind: __sdk::__query_builder::Col, + pub text: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BigFishAgentMessage { + type Cols = BigFishAgentMessageCols; + fn cols(table_name: &'static str) -> Self::Cols { + BigFishAgentMessageCols { + message_id: __sdk::__query_builder::Col::new(table_name, "message_id"), + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + role: __sdk::__query_builder::Col::new(table_name, "role"), + kind: __sdk::__query_builder::Col::new(table_name, "kind"), + text: __sdk::__query_builder::Col::new(table_name, "text"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + + } + } +} + +/// Indexed column accessor struct for the table `BigFishAgentMessage`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BigFishAgentMessageIxCols { + pub message_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BigFishAgentMessage { + type IxCols = BigFishAgentMessageIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BigFishAgentMessageIxCols { + message_id: __sdk::__query_builder::IxCol::new(table_name, "message_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BigFishAgentMessage {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_item_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_item_type.rs new file mode 100644 index 00000000..80ecc407 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_item_type.rs @@ -0,0 +1,27 @@ +// 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::big_fish_anchor_status_type::BigFishAnchorStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAnchorItem { + pub key: String, + pub label: String, + pub value: String, + pub status: BigFishAnchorStatus, +} + + +impl __sdk::InModule for BigFishAnchorItem { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_pack_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_pack_type.rs new file mode 100644 index 00000000..6f93262e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_pack_type.rs @@ -0,0 +1,27 @@ +// 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::big_fish_anchor_item_type::BigFishAnchorItem; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAnchorPack { + pub gameplay_promise: BigFishAnchorItem, + pub ecology_visual_theme: BigFishAnchorItem, + pub growth_ladder: BigFishAnchorItem, + pub risk_tempo: BigFishAnchorItem, +} + + +impl __sdk::InModule for BigFishAnchorPack { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_status_type.rs new file mode 100644 index 00000000..d1ed8bb3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_anchor_status_type.rs @@ -0,0 +1,31 @@ +// 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 BigFishAnchorStatus { + Confirmed, + + Inferred, + + Missing, + + Locked, + +} + + + +impl __sdk::InModule for BigFishAnchorStatus { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_coverage_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_coverage_type.rs new file mode 100644 index 00000000..c1b3429a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_coverage_type.rs @@ -0,0 +1,28 @@ +// 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 BigFishAssetCoverage { + pub level_main_image_ready_count: u32, + pub level_motion_ready_count: u32, + pub background_ready: bool, + pub required_level_count: u32, + pub publish_ready: bool, + pub blockers: Vec::, +} + + +impl __sdk::InModule for BigFishAssetCoverage { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_type.rs new file mode 100644 index 00000000..e57057d1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_generate_input_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::big_fish_asset_kind_type::BigFishAssetKind; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAssetGenerateInput { + pub session_id: String, + pub owner_user_id: String, + pub asset_kind: BigFishAssetKind, + pub level: Option::, + pub motion_key: Option::, + pub generated_at_micros: i64, +} + + +impl __sdk::InModule for BigFishAssetGenerateInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_kind_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_kind_type.rs new file mode 100644 index 00000000..a5d6c3ed --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_kind_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, +}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum BigFishAssetKind { + LevelMainImage, + + LevelMotion, + + StageBackground, + +} + + + +impl __sdk::InModule for BigFishAssetKind { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_snapshot_type.rs new file mode 100644 index 00000000..911e51fc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_snapshot_type.rs @@ -0,0 +1,33 @@ +// 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::big_fish_asset_kind_type::BigFishAssetKind; +use super::big_fish_asset_status_type::BigFishAssetStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAssetSlotSnapshot { + pub slot_id: String, + pub session_id: String, + pub asset_kind: BigFishAssetKind, + pub level: Option::, + pub motion_key: Option::, + pub status: BigFishAssetStatus, + pub asset_url: Option::, + pub prompt_snapshot: String, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for BigFishAssetSlotSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_table.rs new file mode 100644 index 00000000..8b296390 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_table.rs @@ -0,0 +1,165 @@ +// 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::big_fish_asset_slot_type::BigFishAssetSlot; +use super::big_fish_asset_kind_type::BigFishAssetKind; +use super::big_fish_asset_status_type::BigFishAssetStatus; + +/// Table handle for the table `big_fish_asset_slot`. +/// +/// Obtain a handle from the [`BigFishAssetSlotTableAccess::big_fish_asset_slot`] method on [`super::RemoteTables`], +/// like `ctx.db.big_fish_asset_slot()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.big_fish_asset_slot().on_insert(...)`. +pub struct BigFishAssetSlotTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `big_fish_asset_slot`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BigFishAssetSlotTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BigFishAssetSlotTableHandle`], which mediates access to the table `big_fish_asset_slot`. + fn big_fish_asset_slot(&self) -> BigFishAssetSlotTableHandle<'_>; +} + +impl BigFishAssetSlotTableAccess for super::RemoteTables { + fn big_fish_asset_slot(&self) -> BigFishAssetSlotTableHandle<'_> { + BigFishAssetSlotTableHandle { + imp: self.imp.get_table::("big_fish_asset_slot"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BigFishAssetSlotInsertCallbackId(__sdk::CallbackId); +pub struct BigFishAssetSlotDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BigFishAssetSlotTableHandle<'ctx> { + type Row = BigFishAssetSlot; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = BigFishAssetSlotInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishAssetSlotInsertCallbackId { + BigFishAssetSlotInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BigFishAssetSlotInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BigFishAssetSlotDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishAssetSlotDeleteCallbackId { + BigFishAssetSlotDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BigFishAssetSlotDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BigFishAssetSlotUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BigFishAssetSlotTableHandle<'ctx> { + type UpdateCallbackId = BigFishAssetSlotUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BigFishAssetSlotUpdateCallbackId { + BigFishAssetSlotUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BigFishAssetSlotUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `slot_id` unique index on the table `big_fish_asset_slot`, + /// which allows point queries on the field of the same name + /// via the [`BigFishAssetSlotSlotIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.big_fish_asset_slot().slot_id().find(...)`. + pub struct BigFishAssetSlotSlotIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> BigFishAssetSlotTableHandle<'ctx> { + /// Get a handle on the `slot_id` unique index on the table `big_fish_asset_slot`. + pub fn slot_id(&self) -> BigFishAssetSlotSlotIdUnique<'ctx> { + BigFishAssetSlotSlotIdUnique { + imp: self.imp.get_unique_constraint::("slot_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> BigFishAssetSlotSlotIdUnique<'ctx> { + /// Find the subscribed row whose `slot_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::("big_fish_asset_slot"); + _table.add_unique_constraint::("slot_id", |row| &row.slot_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 `BigFishAssetSlot`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait big_fish_asset_slotQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BigFishAssetSlot`. + fn big_fish_asset_slot(&self) -> __sdk::__query_builder::Table; + } + + impl big_fish_asset_slotQueryTableAccess for __sdk::QueryTableAccessor { + fn big_fish_asset_slot(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("big_fish_asset_slot") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_type.rs new file mode 100644 index 00000000..a6cb0813 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_slot_type.rs @@ -0,0 +1,88 @@ +// 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::big_fish_asset_kind_type::BigFishAssetKind; +use super::big_fish_asset_status_type::BigFishAssetStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishAssetSlot { + pub slot_id: String, + pub session_id: String, + pub asset_kind: BigFishAssetKind, + pub level: Option::, + pub motion_key: Option::, + pub status: BigFishAssetStatus, + pub asset_url: Option::, + pub prompt_snapshot: String, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for BigFishAssetSlot { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `BigFishAssetSlot`. +/// +/// Provides typed access to columns for query building. +pub struct BigFishAssetSlotCols { + pub slot_id: __sdk::__query_builder::Col, + pub session_id: __sdk::__query_builder::Col, + pub asset_kind: __sdk::__query_builder::Col, + pub level: __sdk::__query_builder::Col>, + pub motion_key: __sdk::__query_builder::Col>, + pub status: __sdk::__query_builder::Col, + pub asset_url: __sdk::__query_builder::Col>, + pub prompt_snapshot: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BigFishAssetSlot { + type Cols = BigFishAssetSlotCols; + fn cols(table_name: &'static str) -> Self::Cols { + BigFishAssetSlotCols { + slot_id: __sdk::__query_builder::Col::new(table_name, "slot_id"), + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + asset_kind: __sdk::__query_builder::Col::new(table_name, "asset_kind"), + level: __sdk::__query_builder::Col::new(table_name, "level"), + motion_key: __sdk::__query_builder::Col::new(table_name, "motion_key"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + asset_url: __sdk::__query_builder::Col::new(table_name, "asset_url"), + prompt_snapshot: __sdk::__query_builder::Col::new(table_name, "prompt_snapshot"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + + } + } +} + +/// Indexed column accessor struct for the table `BigFishAssetSlot`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BigFishAssetSlotIxCols { + pub session_id: __sdk::__query_builder::IxCol, + pub slot_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BigFishAssetSlot { + type IxCols = BigFishAssetSlotIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BigFishAssetSlotIxCols { + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + slot_id: __sdk::__query_builder::IxCol::new(table_name, "slot_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BigFishAssetSlot {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_status_type.rs new file mode 100644 index 00000000..25ed0f6a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_asset_status_type.rs @@ -0,0 +1,27 @@ +// 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 BigFishAssetStatus { + Missing, + + Ready, + +} + + + +impl __sdk::InModule for BigFishAssetStatus { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_background_blueprint_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_background_blueprint_type.rs new file mode 100644 index 00000000..bda8e3be --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_background_blueprint_type.rs @@ -0,0 +1,30 @@ +// 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 BigFishBackgroundBlueprint { + pub theme: String, + pub color_mood: String, + pub foreground_hints: String, + pub midground_composition: String, + pub background_depth: String, + pub safe_play_area_hint: String, + pub spawn_edge_hint: String, + pub background_prompt_seed: String, +} + + +impl __sdk::InModule for BigFishBackgroundBlueprint { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_table.rs new file mode 100644 index 00000000..3eff808d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_table.rs @@ -0,0 +1,164 @@ +// 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::big_fish_creation_session_type::BigFishCreationSession; +use super::big_fish_creation_stage_type::BigFishCreationStage; + +/// Table handle for the table `big_fish_creation_session`. +/// +/// Obtain a handle from the [`BigFishCreationSessionTableAccess::big_fish_creation_session`] method on [`super::RemoteTables`], +/// like `ctx.db.big_fish_creation_session()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.big_fish_creation_session().on_insert(...)`. +pub struct BigFishCreationSessionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `big_fish_creation_session`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BigFishCreationSessionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BigFishCreationSessionTableHandle`], which mediates access to the table `big_fish_creation_session`. + fn big_fish_creation_session(&self) -> BigFishCreationSessionTableHandle<'_>; +} + +impl BigFishCreationSessionTableAccess for super::RemoteTables { + fn big_fish_creation_session(&self) -> BigFishCreationSessionTableHandle<'_> { + BigFishCreationSessionTableHandle { + imp: self.imp.get_table::("big_fish_creation_session"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BigFishCreationSessionInsertCallbackId(__sdk::CallbackId); +pub struct BigFishCreationSessionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BigFishCreationSessionTableHandle<'ctx> { + type Row = BigFishCreationSession; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = BigFishCreationSessionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishCreationSessionInsertCallbackId { + BigFishCreationSessionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BigFishCreationSessionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BigFishCreationSessionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishCreationSessionDeleteCallbackId { + BigFishCreationSessionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BigFishCreationSessionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BigFishCreationSessionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BigFishCreationSessionTableHandle<'ctx> { + type UpdateCallbackId = BigFishCreationSessionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BigFishCreationSessionUpdateCallbackId { + BigFishCreationSessionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BigFishCreationSessionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `session_id` unique index on the table `big_fish_creation_session`, + /// which allows point queries on the field of the same name + /// via the [`BigFishCreationSessionSessionIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.big_fish_creation_session().session_id().find(...)`. + pub struct BigFishCreationSessionSessionIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> BigFishCreationSessionTableHandle<'ctx> { + /// Get a handle on the `session_id` unique index on the table `big_fish_creation_session`. + pub fn session_id(&self) -> BigFishCreationSessionSessionIdUnique<'ctx> { + BigFishCreationSessionSessionIdUnique { + imp: self.imp.get_unique_constraint::("session_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> BigFishCreationSessionSessionIdUnique<'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::("big_fish_creation_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 `BigFishCreationSession`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait big_fish_creation_sessionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BigFishCreationSession`. + fn big_fish_creation_session(&self) -> __sdk::__query_builder::Table; + } + + impl big_fish_creation_sessionQueryTableAccess for __sdk::QueryTableAccessor { + fn big_fish_creation_session(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("big_fish_creation_session") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs new file mode 100644 index 00000000..b45590fe --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs @@ -0,0 +1,99 @@ +// 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::big_fish_creation_stage_type::BigFishCreationStage; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishCreationSession { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: BigFishCreationStage, + pub anchor_pack_json: String, + pub draft_json: Option::, + pub asset_coverage_json: String, + pub last_assistant_reply: Option::, + pub publish_ready: bool, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for BigFishCreationSession { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `BigFishCreationSession`. +/// +/// Provides typed access to columns for query building. +pub struct BigFishCreationSessionCols { + 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 anchor_pack_json: __sdk::__query_builder::Col, + pub draft_json: __sdk::__query_builder::Col>, + pub asset_coverage_json: __sdk::__query_builder::Col, + pub last_assistant_reply: __sdk::__query_builder::Col>, + pub publish_ready: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BigFishCreationSession { + type Cols = BigFishCreationSessionCols; + fn cols(table_name: &'static str) -> Self::Cols { + BigFishCreationSessionCols { + 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"), + anchor_pack_json: __sdk::__query_builder::Col::new(table_name, "anchor_pack_json"), + draft_json: __sdk::__query_builder::Col::new(table_name, "draft_json"), + asset_coverage_json: __sdk::__query_builder::Col::new(table_name, "asset_coverage_json"), + last_assistant_reply: __sdk::__query_builder::Col::new(table_name, "last_assistant_reply"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + 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 `BigFishCreationSession`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BigFishCreationSessionIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BigFishCreationSession { + type IxCols = BigFishCreationSessionIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BigFishCreationSessionIxCols { + 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 BigFishCreationSession {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_stage_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_stage_type.rs new file mode 100644 index 00000000..b96fb5e4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_stage_type.rs @@ -0,0 +1,33 @@ +// 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 BigFishCreationStage { + CollectingAnchors, + + DraftReady, + + AssetRefining, + + ReadyToPublish, + + Published, + +} + + + +impl __sdk::InModule for BigFishCreationStage { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_draft_compile_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_draft_compile_input_type.rs new file mode 100644 index 00000000..25792484 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_draft_compile_input_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub compiled_at_micros: i64, +} + + +impl __sdk::InModule for BigFishDraftCompileInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_game_draft_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_game_draft_type.rs new file mode 100644 index 00000000..43810d9b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_game_draft_type.rs @@ -0,0 +1,32 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + +use super::big_fish_level_blueprint_type::BigFishLevelBlueprint; +use super::big_fish_background_blueprint_type::BigFishBackgroundBlueprint; +use super::big_fish_runtime_params_type::BigFishRuntimeParams; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishGameDraft { + pub title: String, + pub subtitle: String, + pub core_fun: String, + pub ecology_theme: String, + pub levels: Vec::, + pub background: BigFishBackgroundBlueprint, + pub runtime_params: BigFishRuntimeParams, +} + + +impl __sdk::InModule for BigFishGameDraft { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_level_blueprint_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_level_blueprint_type.rs new file mode 100644 index 00000000..3953e1ff --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_level_blueprint_type.rs @@ -0,0 +1,33 @@ +// 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 BigFishLevelBlueprint { + pub level: u32, + pub name: String, + pub one_line_fantasy: String, + pub silhouette_direction: String, + pub size_ratio: f32, + pub visual_prompt_seed: String, + pub motion_prompt_seed: String, + pub merge_source_level: Option::, + pub prey_window: Vec::, + pub threat_window: Vec::, + pub is_final_level: bool, +} + + +impl __sdk::InModule for BigFishLevelBlueprint { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_message_submit_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_message_submit_input_type.rs new file mode 100644 index 00000000..40dfe964 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_message_submit_input_type.rs @@ -0,0 +1,28 @@ +// 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 BigFishMessageSubmitInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub assistant_message_id: String, + pub submitted_at_micros: i64, +} + + +impl __sdk::InModule for BigFishMessageSubmitInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_publish_input_type.rs new file mode 100644 index 00000000..a1a0fcca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_publish_input_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishPublishInput { + pub session_id: String, + pub owner_user_id: String, + pub published_at_micros: i64, +} + + +impl __sdk::InModule for BigFishPublishInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_get_input_type.rs new file mode 100644 index 00000000..0940b187 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_get_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 BigFishRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for BigFishRunGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_input_submit_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_input_submit_input_type.rs new file mode 100644 index 00000000..ae5f1367 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_input_submit_input_type.rs @@ -0,0 +1,27 @@ +// 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 BigFishRunInputSubmitInput { + pub run_id: String, + pub owner_user_id: String, + pub input_x: f32, + pub input_y: f32, + pub submitted_at_micros: i64, +} + + +impl __sdk::InModule for BigFishRunInputSubmitInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs new file mode 100644 index 00000000..12e6eb95 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_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, +}; + +use super::big_fish_runtime_snapshot_type::BigFishRuntimeSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRunProcedureResult { + pub ok: bool, + pub run: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for BigFishRunProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_start_input_type.rs new file mode 100644 index 00000000..4e166892 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_start_input_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRunStartInput { + pub run_id: String, + pub session_id: String, + pub owner_user_id: String, + pub started_at_micros: i64, +} + + +impl __sdk::InModule for BigFishRunStartInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_status_type.rs new file mode 100644 index 00000000..ab1b80cd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_status_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, +}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum BigFishRunStatus { + Running, + + Won, + + Failed, + +} + + + +impl __sdk::InModule for BigFishRunStatus { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_type.rs new file mode 100644 index 00000000..f1a82061 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_type.rs @@ -0,0 +1,28 @@ +// 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::big_fish_vector_2_type::BigFishVector2; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRuntimeEntity { + pub entity_id: String, + pub level: u32, + pub position: BigFishVector2, + pub radius: f32, + pub offscreen_seconds: f32, +} + + +impl __sdk::InModule for BigFishRuntimeEntity { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_params_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_params_type.rs new file mode 100644 index 00000000..1fece110 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_params_type.rs @@ -0,0 +1,31 @@ +// 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 BigFishRuntimeParams { + pub level_count: u32, + pub merge_count_per_upgrade: u32, + pub spawn_target_count: u32, + pub leader_move_speed: f32, + pub follower_catch_up_speed: f32, + pub offscreen_cull_seconds: f32, + pub prey_spawn_delta_levels: Vec::, + pub threat_spawn_delta_levels: Vec::, + pub win_level: u32, +} + + +impl __sdk::InModule for BigFishRuntimeParams { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_table.rs new file mode 100644 index 00000000..9ed2da19 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_table.rs @@ -0,0 +1,164 @@ +// 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::big_fish_runtime_run_type::BigFishRuntimeRun; +use super::big_fish_run_status_type::BigFishRunStatus; + +/// Table handle for the table `big_fish_runtime_run`. +/// +/// Obtain a handle from the [`BigFishRuntimeRunTableAccess::big_fish_runtime_run`] method on [`super::RemoteTables`], +/// like `ctx.db.big_fish_runtime_run()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.big_fish_runtime_run().on_insert(...)`. +pub struct BigFishRuntimeRunTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `big_fish_runtime_run`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BigFishRuntimeRunTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BigFishRuntimeRunTableHandle`], which mediates access to the table `big_fish_runtime_run`. + fn big_fish_runtime_run(&self) -> BigFishRuntimeRunTableHandle<'_>; +} + +impl BigFishRuntimeRunTableAccess for super::RemoteTables { + fn big_fish_runtime_run(&self) -> BigFishRuntimeRunTableHandle<'_> { + BigFishRuntimeRunTableHandle { + imp: self.imp.get_table::("big_fish_runtime_run"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BigFishRuntimeRunInsertCallbackId(__sdk::CallbackId); +pub struct BigFishRuntimeRunDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BigFishRuntimeRunTableHandle<'ctx> { + type Row = BigFishRuntimeRun; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = BigFishRuntimeRunInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishRuntimeRunInsertCallbackId { + BigFishRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BigFishRuntimeRunInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BigFishRuntimeRunDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishRuntimeRunDeleteCallbackId { + BigFishRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BigFishRuntimeRunDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct BigFishRuntimeRunUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for BigFishRuntimeRunTableHandle<'ctx> { + type UpdateCallbackId = BigFishRuntimeRunUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> BigFishRuntimeRunUpdateCallbackId { + BigFishRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: BigFishRuntimeRunUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `run_id` unique index on the table `big_fish_runtime_run`, + /// which allows point queries on the field of the same name + /// via the [`BigFishRuntimeRunRunIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.big_fish_runtime_run().run_id().find(...)`. + pub struct BigFishRuntimeRunRunIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> BigFishRuntimeRunTableHandle<'ctx> { + /// Get a handle on the `run_id` unique index on the table `big_fish_runtime_run`. + pub fn run_id(&self) -> BigFishRuntimeRunRunIdUnique<'ctx> { + BigFishRuntimeRunRunIdUnique { + imp: self.imp.get_unique_constraint::("run_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> BigFishRuntimeRunRunIdUnique<'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::("big_fish_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 `BigFishRuntimeRun`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait big_fish_runtime_runQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BigFishRuntimeRun`. + fn big_fish_runtime_run(&self) -> __sdk::__query_builder::Table; + } + + impl big_fish_runtime_runQueryTableAccess for __sdk::QueryTableAccessor { + fn big_fish_runtime_run(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("big_fish_runtime_run") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs new file mode 100644 index 00000000..20ac1aa1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs @@ -0,0 +1,92 @@ +// 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::big_fish_run_status_type::BigFishRunStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRuntimeRun { + pub run_id: String, + pub session_id: String, + pub owner_user_id: String, + pub status: BigFishRunStatus, + pub snapshot_json: String, + pub last_input_x: f32, + pub last_input_y: f32, + pub tick: u64, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for BigFishRuntimeRun { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `BigFishRuntimeRun`. +/// +/// Provides typed access to columns for query building. +pub struct BigFishRuntimeRunCols { + pub run_id: __sdk::__query_builder::Col, + pub session_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub status: __sdk::__query_builder::Col, + pub snapshot_json: __sdk::__query_builder::Col, + pub last_input_x: __sdk::__query_builder::Col, + pub last_input_y: __sdk::__query_builder::Col, + pub tick: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for BigFishRuntimeRun { + type Cols = BigFishRuntimeRunCols; + fn cols(table_name: &'static str) -> Self::Cols { + BigFishRuntimeRunCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"), + last_input_x: __sdk::__query_builder::Col::new(table_name, "last_input_x"), + last_input_y: __sdk::__query_builder::Col::new(table_name, "last_input_y"), + tick: __sdk::__query_builder::Col::new(table_name, "tick"), + 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 `BigFishRuntimeRun`. +/// +/// Provides typed access to indexed columns for query building. +pub struct BigFishRuntimeRunIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for BigFishRuntimeRun { + type IxCols = BigFishRuntimeRunIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + BigFishRuntimeRunIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for BigFishRuntimeRun {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs new file mode 100644 index 00000000..0a367449 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs @@ -0,0 +1,38 @@ +// 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::big_fish_run_status_type::BigFishRunStatus; +use super::big_fish_runtime_entity_type::BigFishRuntimeEntity; +use super::big_fish_vector_2_type::BigFishVector2; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRuntimeSnapshot { + pub run_id: String, + pub session_id: String, + pub status: BigFishRunStatus, + pub tick: u64, + pub player_level: u32, + pub win_level: u32, + pub leader_entity_id: Option::, + pub owned_entities: Vec::, + pub wild_entities: Vec::, + pub camera_center: BigFishVector2, + pub last_input: BigFishVector2, + pub event_log: Vec::, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for BigFishRuntimeSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_create_input_type.rs new file mode 100644 index 00000000..f7ae7eb7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_create_input_type.rs @@ -0,0 +1,28 @@ +// 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 BigFishSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + + +impl __sdk::InModule for BigFishSessionCreateInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_get_input_type.rs new file mode 100644 index 00000000..88ef9cfa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_get_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 BigFishSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for BigFishSessionGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_procedure_result_type.rs new file mode 100644 index 00000000..db8af7c7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_procedure_result_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, +}; + +use super::big_fish_session_snapshot_type::BigFishSessionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishSessionProcedureResult { + pub ok: bool, + pub session: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for BigFishSessionProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_snapshot_type.rs new file mode 100644 index 00000000..abff56b6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_session_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::big_fish_creation_stage_type::BigFishCreationStage; +use super::big_fish_anchor_pack_type::BigFishAnchorPack; +use super::big_fish_game_draft_type::BigFishGameDraft; +use super::big_fish_asset_slot_snapshot_type::BigFishAssetSlotSnapshot; +use super::big_fish_asset_coverage_type::BigFishAssetCoverage; +use super::big_fish_agent_message_snapshot_type::BigFishAgentMessageSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: BigFishCreationStage, + pub anchor_pack: BigFishAnchorPack, + pub draft: Option::, + pub asset_slots: Vec::, + pub asset_coverage: BigFishAssetCoverage, + pub messages: Vec::, + pub last_assistant_reply: Option::, + pub publish_ready: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for BigFishSessionSnapshot { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs new file mode 100644 index 00000000..534f83e9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_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 BigFishVector2 { + pub x: f32, + pub y: f32, +} + + +impl __sdk::InModule for BigFishVector2 { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs new file mode 100644 index 00000000..ba45db0a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_draft_compile_input_type::BigFishDraftCompileInput; +use super::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct CompileBigFishDraftArgs { + pub input: BigFishDraftCompileInput, +} + + +impl __sdk::InModule for CompileBigFishDraftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `compile_big_fish_draft`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait compile_big_fish_draft { + fn compile_big_fish_draft(&self, input: BigFishDraftCompileInput, +) { + self.compile_big_fish_draft_then(input, |_, _| {}); + } + + fn compile_big_fish_draft_then( + &self, + input: BigFishDraftCompileInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl compile_big_fish_draft for super::RemoteProcedures { + fn compile_big_fish_draft_then( + &self, + input: BigFishDraftCompileInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "compile_big_fish_draft", + CompileBigFishDraftArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs new file mode 100644 index 00000000..d90769dc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_draft_compile_input_type::PuzzleDraftCompileInput; +use super::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct CompilePuzzleAgentDraftArgs { + pub input: PuzzleDraftCompileInput, +} + + +impl __sdk::InModule for CompilePuzzleAgentDraftArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `compile_puzzle_agent_draft`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait compile_puzzle_agent_draft { + fn compile_puzzle_agent_draft(&self, input: PuzzleDraftCompileInput, +) { + self.compile_puzzle_agent_draft_then(input, |_, _| {}); + } + + fn compile_puzzle_agent_draft_then( + &self, + input: PuzzleDraftCompileInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl compile_puzzle_agent_draft for super::RemoteProcedures { + fn compile_puzzle_agent_draft_then( + &self, + input: PuzzleDraftCompileInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "compile_puzzle_agent_draft", + CompilePuzzleAgentDraftArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs new file mode 100644 index 00000000..cf4398e6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +use super::big_fish_session_create_input_type::BigFishSessionCreateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct CreateBigFishSessionArgs { + pub input: BigFishSessionCreateInput, +} + + +impl __sdk::InModule for CreateBigFishSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_big_fish_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_big_fish_session { + fn create_big_fish_session(&self, input: BigFishSessionCreateInput, +) { + self.create_big_fish_session_then(input, |_, _| {}); + } + + fn create_big_fish_session_then( + &self, + input: BigFishSessionCreateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl create_big_fish_session for super::RemoteProcedures { + fn create_big_fish_session_then( + &self, + input: BigFishSessionCreateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "create_big_fish_session", + CreateBigFishSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs new file mode 100644 index 00000000..d5c79fa4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +use super::puzzle_agent_session_create_input_type::PuzzleAgentSessionCreateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct CreatePuzzleAgentSessionArgs { + pub input: PuzzleAgentSessionCreateInput, +} + + +impl __sdk::InModule for CreatePuzzleAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `create_puzzle_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait create_puzzle_agent_session { + fn create_puzzle_agent_session(&self, input: PuzzleAgentSessionCreateInput, +) { + self.create_puzzle_agent_session_then(input, |_, _| {}); + } + + fn create_puzzle_agent_session_then( + &self, + input: PuzzleAgentSessionCreateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl create_puzzle_agent_session for super::RemoteProcedures { + fn create_puzzle_agent_session_then( + &self, + input: PuzzleAgentSessionCreateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "create_puzzle_agent_session", + CreatePuzzleAgentSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs new file mode 100644 index 00000000..49c4ee1c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; +use super::puzzle_run_drag_input_type::PuzzleRunDragInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct DragPuzzlePieceOrGroupArgs { + pub input: PuzzleRunDragInput, +} + + +impl __sdk::InModule for DragPuzzlePieceOrGroupArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `drag_puzzle_piece_or_group`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait drag_puzzle_piece_or_group { + fn drag_puzzle_piece_or_group(&self, input: PuzzleRunDragInput, +) { + self.drag_puzzle_piece_or_group_then(input, |_, _| {}); + } + + fn drag_puzzle_piece_or_group_then( + &self, + input: PuzzleRunDragInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl drag_puzzle_piece_or_group for super::RemoteProcedures { + fn drag_puzzle_piece_or_group_then( + &self, + input: PuzzleRunDragInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( + "drag_puzzle_piece_or_group", + DragPuzzlePieceOrGroupArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs new file mode 100644 index 00000000..6ee820f7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +use super::big_fish_asset_generate_input_type::BigFishAssetGenerateInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GenerateBigFishAssetArgs { + pub input: BigFishAssetGenerateInput, +} + + +impl __sdk::InModule for GenerateBigFishAssetArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `generate_big_fish_asset`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait generate_big_fish_asset { + fn generate_big_fish_asset(&self, input: BigFishAssetGenerateInput, +) { + self.generate_big_fish_asset_then(input, |_, _| {}); + } + + fn generate_big_fish_asset_then( + &self, + input: BigFishAssetGenerateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl generate_big_fish_asset for super::RemoteProcedures { + fn generate_big_fish_asset_then( + &self, + input: BigFishAssetGenerateInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "generate_big_fish_asset", + GenerateBigFishAssetArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_run_procedure.rs new file mode 100644 index 00000000..a16bbc76 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_run_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_run_get_input_type::BigFishRunGetInput; +use super::big_fish_run_procedure_result_type::BigFishRunProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetBigFishRunArgs { + pub input: BigFishRunGetInput, +} + + +impl __sdk::InModule for GetBigFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_big_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_big_fish_run { + fn get_big_fish_run(&self, input: BigFishRunGetInput, +) { + self.get_big_fish_run_then(input, |_, _| {}); + } + + fn get_big_fish_run_then( + &self, + input: BigFishRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_big_fish_run for super::RemoteProcedures { + fn get_big_fish_run_then( + &self, + input: BigFishRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishRunProcedureResult>( + "get_big_fish_run", + GetBigFishRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs new file mode 100644 index 00000000..f54dc9e4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +use super::big_fish_session_get_input_type::BigFishSessionGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetBigFishSessionArgs { + pub input: BigFishSessionGetInput, +} + + +impl __sdk::InModule for GetBigFishSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_big_fish_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_big_fish_session { + fn get_big_fish_session(&self, input: BigFishSessionGetInput, +) { + self.get_big_fish_session_then(input, |_, _| {}); + } + + fn get_big_fish_session_then( + &self, + input: BigFishSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_big_fish_session for super::RemoteProcedures { + fn get_big_fish_session_then( + &self, + input: BigFishSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "get_big_fish_session", + GetBigFishSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs new file mode 100644 index 00000000..8946764b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +use super::puzzle_agent_session_get_input_type::PuzzleAgentSessionGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetPuzzleAgentSessionArgs { + pub input: PuzzleAgentSessionGetInput, +} + + +impl __sdk::InModule for GetPuzzleAgentSessionArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_puzzle_agent_session`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_puzzle_agent_session { + fn get_puzzle_agent_session(&self, input: PuzzleAgentSessionGetInput, +) { + self.get_puzzle_agent_session_then(input, |_, _| {}); + } + + fn get_puzzle_agent_session_then( + &self, + input: PuzzleAgentSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_puzzle_agent_session for super::RemoteProcedures { + fn get_puzzle_agent_session_then( + &self, + input: PuzzleAgentSessionGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "get_puzzle_agent_session", + GetPuzzleAgentSessionArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs new file mode 100644 index 00000000..0c78ac7c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_work_get_input_type::PuzzleWorkGetInput; +use super::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetPuzzleGalleryDetailArgs { + pub input: PuzzleWorkGetInput, +} + + +impl __sdk::InModule for GetPuzzleGalleryDetailArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_puzzle_gallery_detail`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_puzzle_gallery_detail { + fn get_puzzle_gallery_detail(&self, input: PuzzleWorkGetInput, +) { + self.get_puzzle_gallery_detail_then(input, |_, _| {}); + } + + fn get_puzzle_gallery_detail_then( + &self, + input: PuzzleWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_puzzle_gallery_detail for super::RemoteProcedures { + fn get_puzzle_gallery_detail_then( + &self, + input: PuzzleWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( + "get_puzzle_gallery_detail", + GetPuzzleGalleryDetailArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs new file mode 100644 index 00000000..c409b79d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; +use super::puzzle_run_get_input_type::PuzzleRunGetInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetPuzzleRunArgs { + pub input: PuzzleRunGetInput, +} + + +impl __sdk::InModule for GetPuzzleRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_puzzle_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_puzzle_run { + fn get_puzzle_run(&self, input: PuzzleRunGetInput, +) { + self.get_puzzle_run_then(input, |_, _| {}); + } + + fn get_puzzle_run_then( + &self, + input: PuzzleRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_puzzle_run for super::RemoteProcedures { + fn get_puzzle_run_then( + &self, + input: PuzzleRunGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( + "get_puzzle_run", + GetPuzzleRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs new file mode 100644 index 00000000..c21d1048 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_work_get_input_type::PuzzleWorkGetInput; +use super::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct GetPuzzleWorkDetailArgs { + pub input: PuzzleWorkGetInput, +} + + +impl __sdk::InModule for GetPuzzleWorkDetailArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_puzzle_work_detail`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_puzzle_work_detail { + fn get_puzzle_work_detail(&self, input: PuzzleWorkGetInput, +) { + self.get_puzzle_work_detail_then(input, |_, _| {}); + } + + fn get_puzzle_work_detail_then( + &self, + input: PuzzleWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl get_puzzle_work_detail for super::RemoteProcedures { + fn get_puzzle_work_detail_then( + &self, + input: PuzzleWorkGetInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( + "get_puzzle_work_detail", + GetPuzzleWorkDetailArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs new file mode 100644 index 00000000..329e53a4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs @@ -0,0 +1,53 @@ +// 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::puzzle_works_procedure_result_type::PuzzleWorksProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct ListPuzzleGalleryArgs { + } + + +impl __sdk::InModule for ListPuzzleGalleryArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_puzzle_gallery`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_puzzle_gallery { + fn list_puzzle_gallery(&self, ) { + self.list_puzzle_gallery_then( |_, _| {}); + } + + fn list_puzzle_gallery_then( + &self, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl list_puzzle_gallery for super::RemoteProcedures { + fn list_puzzle_gallery_then( + &self, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorksProcedureResult>( + "list_puzzle_gallery", + ListPuzzleGalleryArgs { }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs new file mode 100644 index 00000000..980a3698 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_works_procedure_result_type::PuzzleWorksProcedureResult; +use super::puzzle_works_list_input_type::PuzzleWorksListInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct ListPuzzleWorksArgs { + pub input: PuzzleWorksListInput, +} + + +impl __sdk::InModule for ListPuzzleWorksArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_puzzle_works`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_puzzle_works { + fn list_puzzle_works(&self, input: PuzzleWorksListInput, +) { + self.list_puzzle_works_then(input, |_, _| {}); + } + + fn list_puzzle_works_then( + &self, + input: PuzzleWorksListInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl list_puzzle_works for super::RemoteProcedures { + fn list_puzzle_works_then( + &self, + input: PuzzleWorksListInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorksProcedureResult>( + "list_puzzle_works", + ListPuzzleWorksArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 9b0e23e0..2ab529c8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -51,6 +51,41 @@ pub mod battle_state_procedure_result_type; pub mod battle_state_query_input_type; pub mod battle_state_snapshot_type; pub mod battle_status_type; +pub mod big_fish_agent_message_type; +pub mod big_fish_agent_message_kind_type; +pub mod big_fish_agent_message_role_type; +pub mod big_fish_agent_message_snapshot_type; +pub mod big_fish_anchor_item_type; +pub mod big_fish_anchor_pack_type; +pub mod big_fish_anchor_status_type; +pub mod big_fish_asset_coverage_type; +pub mod big_fish_asset_generate_input_type; +pub mod big_fish_asset_kind_type; +pub mod big_fish_asset_slot_type; +pub mod big_fish_asset_slot_snapshot_type; +pub mod big_fish_asset_status_type; +pub mod big_fish_background_blueprint_type; +pub mod big_fish_creation_session_type; +pub mod big_fish_creation_stage_type; +pub mod big_fish_draft_compile_input_type; +pub mod big_fish_game_draft_type; +pub mod big_fish_level_blueprint_type; +pub mod big_fish_message_submit_input_type; +pub mod big_fish_publish_input_type; +pub mod big_fish_run_get_input_type; +pub mod big_fish_run_input_submit_input_type; +pub mod big_fish_run_procedure_result_type; +pub mod big_fish_run_start_input_type; +pub mod big_fish_run_status_type; +pub mod big_fish_runtime_entity_type; +pub mod big_fish_runtime_params_type; +pub mod big_fish_runtime_run_type; +pub mod big_fish_runtime_snapshot_type; +pub mod big_fish_session_create_input_type; +pub mod big_fish_session_get_input_type; +pub mod big_fish_session_procedure_result_type; +pub mod big_fish_session_snapshot_type; +pub mod big_fish_vector_2_type; pub mod chapter_pace_band_type; pub mod chapter_progression_type; pub mod chapter_progression_get_input_type; @@ -142,6 +177,33 @@ pub mod profile_dashboard_state_type; pub mod profile_played_world_type; pub mod profile_save_archive_type; pub mod profile_wallet_ledger_type; +pub mod puzzle_agent_message_kind_type; +pub mod puzzle_agent_message_role_type; +pub mod puzzle_agent_message_row_type; +pub mod puzzle_agent_message_submit_input_type; +pub mod puzzle_agent_session_create_input_type; +pub mod puzzle_agent_session_get_input_type; +pub mod puzzle_agent_session_procedure_result_type; +pub mod puzzle_agent_session_row_type; +pub mod puzzle_agent_stage_type; +pub mod puzzle_draft_compile_input_type; +pub mod puzzle_generated_images_save_input_type; +pub mod puzzle_publication_status_type; +pub mod puzzle_publish_input_type; +pub mod puzzle_run_drag_input_type; +pub mod puzzle_run_get_input_type; +pub mod puzzle_run_next_level_input_type; +pub mod puzzle_run_procedure_result_type; +pub mod puzzle_run_start_input_type; +pub mod puzzle_run_swap_input_type; +pub mod puzzle_runtime_run_row_type; +pub mod puzzle_select_cover_image_input_type; +pub mod puzzle_work_get_input_type; +pub mod puzzle_work_procedure_result_type; +pub mod puzzle_work_profile_row_type; +pub mod puzzle_work_upsert_input_type; +pub mod puzzle_works_list_input_type; +pub mod puzzle_works_procedure_result_type; pub mod quest_completion_ack_input_type; pub mod quest_hostile_npc_defeated_signal_type; pub mod quest_item_delivered_signal_type; @@ -271,6 +333,10 @@ pub mod ai_text_chunk_table; pub mod asset_entity_binding_table; pub mod asset_object_table; pub mod battle_state_table; +pub mod big_fish_agent_message_table; +pub mod big_fish_asset_slot_table; +pub mod big_fish_creation_session_table; +pub mod big_fish_runtime_run_table; pub mod chapter_progression_table; pub mod custom_world_agent_message_table; pub mod custom_world_agent_operation_table; @@ -286,6 +352,10 @@ pub mod profile_dashboard_state_table; pub mod profile_played_world_table; pub mod profile_save_archive_table; pub mod profile_wallet_ledger_table; +pub mod puzzle_agent_message_table; +pub mod puzzle_agent_session_table; +pub mod puzzle_runtime_run_table; +pub mod puzzle_work_profile_table; pub mod quest_log_table; pub mod quest_record_table; pub mod runtime_setting_table; @@ -294,6 +364,7 @@ pub mod story_event_table; pub mod story_session_table; pub mod treasure_record_table; pub mod user_browse_history_table; +pub mod advance_puzzle_next_level_procedure; pub mod append_ai_text_chunk_and_return_procedure; pub mod apply_chapter_progression_ledger_entry_and_return_procedure; pub mod attach_ai_result_reference_and_return_procedure; @@ -301,18 +372,26 @@ pub mod begin_story_session_and_return_procedure; pub mod bind_asset_object_to_entity_and_return_procedure; pub mod cancel_ai_task_and_return_procedure; pub mod clear_platform_browse_history_and_return_procedure; +pub mod compile_big_fish_draft_procedure; pub mod compile_custom_world_published_profile_procedure; +pub mod compile_puzzle_agent_draft_procedure; pub mod complete_ai_stage_and_return_procedure; pub mod complete_ai_task_and_return_procedure; pub mod confirm_asset_object_and_return_procedure; pub mod continue_story_and_return_procedure; pub mod create_ai_task_and_return_procedure; pub mod create_battle_state_and_return_procedure; +pub mod create_big_fish_session_procedure; pub mod create_custom_world_agent_session_procedure; +pub mod create_puzzle_agent_session_procedure; pub mod delete_runtime_snapshot_and_return_procedure; +pub mod drag_puzzle_piece_or_group_procedure; pub mod execute_custom_world_agent_action_procedure; pub mod fail_ai_task_and_return_procedure; +pub mod generate_big_fish_asset_procedure; pub mod get_battle_state_procedure; +pub mod get_big_fish_run_procedure; +pub mod get_big_fish_session_procedure; pub mod get_chapter_progression_procedure; pub mod get_custom_world_agent_card_detail_procedure; pub mod get_custom_world_agent_operation_procedure; @@ -322,6 +401,10 @@ pub mod get_custom_world_library_detail_procedure; pub mod get_player_progression_or_default_procedure; pub mod get_profile_dashboard_procedure; pub mod get_profile_play_stats_procedure; +pub mod get_puzzle_agent_session_procedure; +pub mod get_puzzle_gallery_detail_procedure; +pub mod get_puzzle_run_procedure; +pub mod get_puzzle_work_detail_procedure; pub mod get_runtime_inventory_state_procedure; pub mod get_runtime_setting_or_default_procedure; pub mod get_runtime_snapshot_procedure; @@ -333,16 +416,29 @@ pub mod list_custom_world_works_procedure; pub mod list_platform_browse_history_procedure; pub mod list_profile_save_archives_procedure; pub mod list_profile_wallet_ledger_procedure; +pub mod list_puzzle_gallery_procedure; +pub mod list_puzzle_works_procedure; +pub mod publish_big_fish_game_procedure; pub mod publish_custom_world_profile_and_return_procedure; pub mod publish_custom_world_world_procedure; +pub mod publish_puzzle_work_procedure; pub mod resolve_combat_action_and_return_procedure; pub mod resolve_npc_battle_interaction_and_return_procedure; pub mod resolve_npc_interaction_and_return_procedure; pub mod resolve_npc_social_action_and_return_procedure; pub mod resolve_treasure_interaction_and_return_procedure; pub mod resume_profile_save_archive_and_return_procedure; +pub mod save_puzzle_generated_images_procedure; +pub mod select_puzzle_cover_image_procedure; +pub mod start_big_fish_run_procedure; +pub mod start_puzzle_run_procedure; +pub mod submit_big_fish_input_procedure; +pub mod submit_big_fish_message_procedure; pub mod submit_custom_world_agent_message_procedure; +pub mod submit_puzzle_agent_message_procedure; +pub mod swap_puzzle_pieces_procedure; pub mod unpublish_custom_world_profile_and_return_procedure; +pub mod update_puzzle_work_procedure; pub mod upsert_chapter_progression_and_return_procedure; pub mod upsert_custom_world_profile_and_return_procedure; pub mod upsert_npc_state_and_return_procedure; @@ -390,6 +486,41 @@ pub use battle_state_procedure_result_type::BattleStateProcedureResult; pub use battle_state_query_input_type::BattleStateQueryInput; pub use battle_state_snapshot_type::BattleStateSnapshot; pub use battle_status_type::BattleStatus; +pub use big_fish_agent_message_type::BigFishAgentMessage; +pub use big_fish_agent_message_kind_type::BigFishAgentMessageKind; +pub use big_fish_agent_message_role_type::BigFishAgentMessageRole; +pub use big_fish_agent_message_snapshot_type::BigFishAgentMessageSnapshot; +pub use big_fish_anchor_item_type::BigFishAnchorItem; +pub use big_fish_anchor_pack_type::BigFishAnchorPack; +pub use big_fish_anchor_status_type::BigFishAnchorStatus; +pub use big_fish_asset_coverage_type::BigFishAssetCoverage; +pub use big_fish_asset_generate_input_type::BigFishAssetGenerateInput; +pub use big_fish_asset_kind_type::BigFishAssetKind; +pub use big_fish_asset_slot_type::BigFishAssetSlot; +pub use big_fish_asset_slot_snapshot_type::BigFishAssetSlotSnapshot; +pub use big_fish_asset_status_type::BigFishAssetStatus; +pub use big_fish_background_blueprint_type::BigFishBackgroundBlueprint; +pub use big_fish_creation_session_type::BigFishCreationSession; +pub use big_fish_creation_stage_type::BigFishCreationStage; +pub use big_fish_draft_compile_input_type::BigFishDraftCompileInput; +pub use big_fish_game_draft_type::BigFishGameDraft; +pub use big_fish_level_blueprint_type::BigFishLevelBlueprint; +pub use big_fish_message_submit_input_type::BigFishMessageSubmitInput; +pub use big_fish_publish_input_type::BigFishPublishInput; +pub use big_fish_run_get_input_type::BigFishRunGetInput; +pub use big_fish_run_input_submit_input_type::BigFishRunInputSubmitInput; +pub use big_fish_run_procedure_result_type::BigFishRunProcedureResult; +pub use big_fish_run_start_input_type::BigFishRunStartInput; +pub use big_fish_run_status_type::BigFishRunStatus; +pub use big_fish_runtime_entity_type::BigFishRuntimeEntity; +pub use big_fish_runtime_params_type::BigFishRuntimeParams; +pub use big_fish_runtime_run_type::BigFishRuntimeRun; +pub use big_fish_runtime_snapshot_type::BigFishRuntimeSnapshot; +pub use big_fish_session_create_input_type::BigFishSessionCreateInput; +pub use big_fish_session_get_input_type::BigFishSessionGetInput; +pub use big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +pub use big_fish_session_snapshot_type::BigFishSessionSnapshot; +pub use big_fish_vector_2_type::BigFishVector2; pub use chapter_pace_band_type::ChapterPaceBand; pub use chapter_progression_type::ChapterProgression; pub use chapter_progression_get_input_type::ChapterProgressionGetInput; @@ -481,6 +612,33 @@ pub use profile_dashboard_state_type::ProfileDashboardState; pub use profile_played_world_type::ProfilePlayedWorld; pub use profile_save_archive_type::ProfileSaveArchive; pub use profile_wallet_ledger_type::ProfileWalletLedger; +pub use puzzle_agent_message_kind_type::PuzzleAgentMessageKind; +pub use puzzle_agent_message_role_type::PuzzleAgentMessageRole; +pub use puzzle_agent_message_row_type::PuzzleAgentMessageRow; +pub use puzzle_agent_message_submit_input_type::PuzzleAgentMessageSubmitInput; +pub use puzzle_agent_session_create_input_type::PuzzleAgentSessionCreateInput; +pub use puzzle_agent_session_get_input_type::PuzzleAgentSessionGetInput; +pub use puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +pub use puzzle_agent_session_row_type::PuzzleAgentSessionRow; +pub use puzzle_agent_stage_type::PuzzleAgentStage; +pub use puzzle_draft_compile_input_type::PuzzleDraftCompileInput; +pub use puzzle_generated_images_save_input_type::PuzzleGeneratedImagesSaveInput; +pub use puzzle_publication_status_type::PuzzlePublicationStatus; +pub use puzzle_publish_input_type::PuzzlePublishInput; +pub use puzzle_run_drag_input_type::PuzzleRunDragInput; +pub use puzzle_run_get_input_type::PuzzleRunGetInput; +pub use puzzle_run_next_level_input_type::PuzzleRunNextLevelInput; +pub use puzzle_run_procedure_result_type::PuzzleRunProcedureResult; +pub use puzzle_run_start_input_type::PuzzleRunStartInput; +pub use puzzle_run_swap_input_type::PuzzleRunSwapInput; +pub use puzzle_runtime_run_row_type::PuzzleRuntimeRunRow; +pub use puzzle_select_cover_image_input_type::PuzzleSelectCoverImageInput; +pub use puzzle_work_get_input_type::PuzzleWorkGetInput; +pub use puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; +pub use puzzle_work_profile_row_type::PuzzleWorkProfileRow; +pub use puzzle_work_upsert_input_type::PuzzleWorkUpsertInput; +pub use puzzle_works_list_input_type::PuzzleWorksListInput; +pub use puzzle_works_procedure_result_type::PuzzleWorksProcedureResult; pub use quest_completion_ack_input_type::QuestCompletionAckInput; pub use quest_hostile_npc_defeated_signal_type::QuestHostileNpcDefeatedSignal; pub use quest_item_delivered_signal_type::QuestItemDeliveredSignal; @@ -586,6 +744,10 @@ pub use ai_text_chunk_table::*; pub use asset_entity_binding_table::*; pub use asset_object_table::*; pub use battle_state_table::*; +pub use big_fish_agent_message_table::*; +pub use big_fish_asset_slot_table::*; +pub use big_fish_creation_session_table::*; +pub use big_fish_runtime_run_table::*; pub use chapter_progression_table::*; pub use custom_world_agent_message_table::*; pub use custom_world_agent_operation_table::*; @@ -601,6 +763,10 @@ pub use profile_dashboard_state_table::*; pub use profile_played_world_table::*; pub use profile_save_archive_table::*; pub use profile_wallet_ledger_table::*; +pub use puzzle_agent_message_table::*; +pub use puzzle_agent_session_table::*; +pub use puzzle_runtime_run_table::*; +pub use puzzle_work_profile_table::*; pub use quest_log_table::*; pub use quest_record_table::*; pub use runtime_setting_table::*; @@ -633,6 +799,7 @@ pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile; pub use upsert_chapter_progression_reducer::upsert_chapter_progression; pub use upsert_custom_world_profile_reducer::upsert_custom_world_profile; pub use upsert_npc_state_reducer::upsert_npc_state; +pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level; pub use append_ai_text_chunk_and_return_procedure::append_ai_text_chunk_and_return; pub use apply_chapter_progression_ledger_entry_and_return_procedure::apply_chapter_progression_ledger_entry_and_return; pub use attach_ai_result_reference_and_return_procedure::attach_ai_result_reference_and_return; @@ -640,18 +807,26 @@ pub use begin_story_session_and_return_procedure::begin_story_session_and_return pub use bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return; pub use cancel_ai_task_and_return_procedure::cancel_ai_task_and_return; pub use clear_platform_browse_history_and_return_procedure::clear_platform_browse_history_and_return; +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_puzzle_agent_draft_procedure::compile_puzzle_agent_draft; pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return; pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return; pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return; pub use continue_story_and_return_procedure::continue_story_and_return; pub use create_ai_task_and_return_procedure::create_ai_task_and_return; pub use create_battle_state_and_return_procedure::create_battle_state_and_return; +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_puzzle_agent_session_procedure::create_puzzle_agent_session; pub use delete_runtime_snapshot_and_return_procedure::delete_runtime_snapshot_and_return; +pub use drag_puzzle_piece_or_group_procedure::drag_puzzle_piece_or_group; pub use execute_custom_world_agent_action_procedure::execute_custom_world_agent_action; pub use fail_ai_task_and_return_procedure::fail_ai_task_and_return; +pub use generate_big_fish_asset_procedure::generate_big_fish_asset; pub use get_battle_state_procedure::get_battle_state; +pub use get_big_fish_run_procedure::get_big_fish_run; +pub use get_big_fish_session_procedure::get_big_fish_session; pub use get_chapter_progression_procedure::get_chapter_progression; pub use get_custom_world_agent_card_detail_procedure::get_custom_world_agent_card_detail; pub use get_custom_world_agent_operation_procedure::get_custom_world_agent_operation; @@ -661,6 +836,10 @@ pub use get_custom_world_library_detail_procedure::get_custom_world_library_deta pub use get_player_progression_or_default_procedure::get_player_progression_or_default; pub use get_profile_dashboard_procedure::get_profile_dashboard; pub use get_profile_play_stats_procedure::get_profile_play_stats; +pub use get_puzzle_agent_session_procedure::get_puzzle_agent_session; +pub use get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail; +pub use get_puzzle_run_procedure::get_puzzle_run; +pub use get_puzzle_work_detail_procedure::get_puzzle_work_detail; pub use get_runtime_inventory_state_procedure::get_runtime_inventory_state; pub use get_runtime_setting_or_default_procedure::get_runtime_setting_or_default; pub use get_runtime_snapshot_procedure::get_runtime_snapshot; @@ -672,16 +851,29 @@ pub use list_custom_world_works_procedure::list_custom_world_works; pub use list_platform_browse_history_procedure::list_platform_browse_history; pub use list_profile_save_archives_procedure::list_profile_save_archives; pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger; +pub use list_puzzle_gallery_procedure::list_puzzle_gallery; +pub use list_puzzle_works_procedure::list_puzzle_works; +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_world_procedure::publish_custom_world_world; +pub use publish_puzzle_work_procedure::publish_puzzle_work; pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return; pub use resolve_npc_battle_interaction_and_return_procedure::resolve_npc_battle_interaction_and_return; pub use resolve_npc_interaction_and_return_procedure::resolve_npc_interaction_and_return; pub use resolve_npc_social_action_and_return_procedure::resolve_npc_social_action_and_return; pub use resolve_treasure_interaction_and_return_procedure::resolve_treasure_interaction_and_return; pub use resume_profile_save_archive_and_return_procedure::resume_profile_save_archive_and_return; +pub use save_puzzle_generated_images_procedure::save_puzzle_generated_images; +pub use select_puzzle_cover_image_procedure::select_puzzle_cover_image; +pub use start_big_fish_run_procedure::start_big_fish_run; +pub use start_puzzle_run_procedure::start_puzzle_run; +pub use submit_big_fish_input_procedure::submit_big_fish_input; +pub use submit_big_fish_message_procedure::submit_big_fish_message; pub use submit_custom_world_agent_message_procedure::submit_custom_world_agent_message; +pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message; +pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces; pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return; +pub use update_puzzle_work_procedure::update_puzzle_work; pub use upsert_chapter_progression_and_return_procedure::upsert_chapter_progression_and_return; pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return; pub use upsert_npc_state_and_return_procedure::upsert_npc_state_and_return; @@ -945,6 +1137,10 @@ pub struct DbUpdate { asset_entity_binding: __sdk::TableUpdate, asset_object: __sdk::TableUpdate, battle_state: __sdk::TableUpdate, + big_fish_agent_message: __sdk::TableUpdate, + big_fish_asset_slot: __sdk::TableUpdate, + big_fish_creation_session: __sdk::TableUpdate, + big_fish_runtime_run: __sdk::TableUpdate, chapter_progression: __sdk::TableUpdate, custom_world_agent_message: __sdk::TableUpdate, custom_world_agent_operation: __sdk::TableUpdate, @@ -960,6 +1156,10 @@ pub struct DbUpdate { profile_played_world: __sdk::TableUpdate, profile_save_archive: __sdk::TableUpdate, profile_wallet_ledger: __sdk::TableUpdate, + puzzle_agent_message: __sdk::TableUpdate, + puzzle_agent_session: __sdk::TableUpdate, + puzzle_runtime_run: __sdk::TableUpdate, + puzzle_work_profile: __sdk::TableUpdate, quest_log: __sdk::TableUpdate, quest_record: __sdk::TableUpdate, runtime_setting: __sdk::TableUpdate, @@ -985,6 +1185,10 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?), "asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?), "battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(big_fish_creation_session_table::parse_table_update(table_update)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(big_fish_runtime_run_table::parse_table_update(table_update)?), "chapter_progression" => db_update.chapter_progression.append(chapter_progression_table::parse_table_update(table_update)?), "custom_world_agent_message" => db_update.custom_world_agent_message.append(custom_world_agent_message_table::parse_table_update(table_update)?), "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(custom_world_agent_operation_table::parse_table_update(table_update)?), @@ -1000,6 +1204,10 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "profile_played_world" => db_update.profile_played_world.append(profile_played_world_table::parse_table_update(table_update)?), "profile_save_archive" => db_update.profile_save_archive.append(profile_save_archive_table::parse_table_update(table_update)?), "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(profile_wallet_ledger_table::parse_table_update(table_update)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(puzzle_agent_message_table::parse_table_update(table_update)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(puzzle_agent_session_table::parse_table_update(table_update)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(puzzle_runtime_run_table::parse_table_update(table_update)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(puzzle_work_profile_table::parse_table_update(table_update)?), "quest_log" => db_update.quest_log.append(quest_log_table::parse_table_update(table_update)?), "quest_record" => db_update.quest_record.append(quest_record_table::parse_table_update(table_update)?), "runtime_setting" => db_update.runtime_setting.append(runtime_setting_table::parse_table_update(table_update)?), @@ -1037,6 +1245,10 @@ impl __sdk::DbUpdate for DbUpdate { diff.asset_entity_binding = cache.apply_diff_to_table::("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id); diff.asset_object = cache.apply_diff_to_table::("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id); diff.battle_state = cache.apply_diff_to_table::("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id); + diff.big_fish_agent_message = cache.apply_diff_to_table::("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id); + diff.big_fish_asset_slot = cache.apply_diff_to_table::("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id); + diff.big_fish_creation_session = cache.apply_diff_to_table::("big_fish_creation_session", &self.big_fish_creation_session).with_updates_by_pk(|row| &row.session_id); + diff.big_fish_runtime_run = cache.apply_diff_to_table::("big_fish_runtime_run", &self.big_fish_runtime_run).with_updates_by_pk(|row| &row.run_id); diff.chapter_progression = cache.apply_diff_to_table::("chapter_progression", &self.chapter_progression).with_updates_by_pk(|row| &row.chapter_progression_id); diff.custom_world_agent_message = cache.apply_diff_to_table::("custom_world_agent_message", &self.custom_world_agent_message).with_updates_by_pk(|row| &row.message_id); diff.custom_world_agent_operation = cache.apply_diff_to_table::("custom_world_agent_operation", &self.custom_world_agent_operation).with_updates_by_pk(|row| &row.operation_id); @@ -1052,6 +1264,10 @@ impl __sdk::DbUpdate for DbUpdate { diff.profile_played_world = cache.apply_diff_to_table::("profile_played_world", &self.profile_played_world).with_updates_by_pk(|row| &row.played_world_id); diff.profile_save_archive = cache.apply_diff_to_table::("profile_save_archive", &self.profile_save_archive).with_updates_by_pk(|row| &row.archive_id); diff.profile_wallet_ledger = cache.apply_diff_to_table::("profile_wallet_ledger", &self.profile_wallet_ledger).with_updates_by_pk(|row| &row.wallet_ledger_id); + diff.puzzle_agent_message = cache.apply_diff_to_table::("puzzle_agent_message", &self.puzzle_agent_message).with_updates_by_pk(|row| &row.message_id); + diff.puzzle_agent_session = cache.apply_diff_to_table::("puzzle_agent_session", &self.puzzle_agent_session).with_updates_by_pk(|row| &row.session_id); + diff.puzzle_runtime_run = cache.apply_diff_to_table::("puzzle_runtime_run", &self.puzzle_runtime_run).with_updates_by_pk(|row| &row.run_id); + diff.puzzle_work_profile = cache.apply_diff_to_table::("puzzle_work_profile", &self.puzzle_work_profile).with_updates_by_pk(|row| &row.profile_id); diff.quest_log = cache.apply_diff_to_table::("quest_log", &self.quest_log).with_updates_by_pk(|row| &row.log_id); diff.quest_record = cache.apply_diff_to_table::("quest_record", &self.quest_record).with_updates_by_pk(|row| &row.quest_id); diff.runtime_setting = cache.apply_diff_to_table::("runtime_setting", &self.runtime_setting).with_updates_by_pk(|row| &row.user_id); @@ -1074,6 +1290,10 @@ for table_rows in raw.tables { "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -1089,6 +1309,10 @@ for table_rows in raw.tables { "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -1111,6 +1335,10 @@ for table_rows in raw.tables { "asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -1126,6 +1354,10 @@ for table_rows in raw.tables { "profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -1150,6 +1382,10 @@ pub struct AppliedDiff<'r> { asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>, asset_object: __sdk::TableAppliedDiff<'r, AssetObject>, battle_state: __sdk::TableAppliedDiff<'r, BattleState>, + big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>, + big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>, + big_fish_creation_session: __sdk::TableAppliedDiff<'r, BigFishCreationSession>, + big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>, chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>, custom_world_agent_message: __sdk::TableAppliedDiff<'r, CustomWorldAgentMessage>, custom_world_agent_operation: __sdk::TableAppliedDiff<'r, CustomWorldAgentOperation>, @@ -1165,6 +1401,10 @@ pub struct AppliedDiff<'r> { profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>, profile_save_archive: __sdk::TableAppliedDiff<'r, ProfileSaveArchive>, profile_wallet_ledger: __sdk::TableAppliedDiff<'r, ProfileWalletLedger>, + puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>, + puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>, + puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>, + puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>, quest_log: __sdk::TableAppliedDiff<'r, QuestLog>, quest_record: __sdk::TableAppliedDiff<'r, QuestRecord>, runtime_setting: __sdk::TableAppliedDiff<'r, RuntimeSetting>, @@ -1190,6 +1430,10 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { callbacks.invoke_table_row_callbacks::("asset_entity_binding", &self.asset_entity_binding, event); callbacks.invoke_table_row_callbacks::("asset_object", &self.asset_object, event); callbacks.invoke_table_row_callbacks::("battle_state", &self.battle_state, event); + callbacks.invoke_table_row_callbacks::("big_fish_agent_message", &self.big_fish_agent_message, event); + callbacks.invoke_table_row_callbacks::("big_fish_asset_slot", &self.big_fish_asset_slot, event); + callbacks.invoke_table_row_callbacks::("big_fish_creation_session", &self.big_fish_creation_session, event); + callbacks.invoke_table_row_callbacks::("big_fish_runtime_run", &self.big_fish_runtime_run, event); callbacks.invoke_table_row_callbacks::("chapter_progression", &self.chapter_progression, event); callbacks.invoke_table_row_callbacks::("custom_world_agent_message", &self.custom_world_agent_message, event); callbacks.invoke_table_row_callbacks::("custom_world_agent_operation", &self.custom_world_agent_operation, event); @@ -1205,6 +1449,10 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { callbacks.invoke_table_row_callbacks::("profile_played_world", &self.profile_played_world, event); callbacks.invoke_table_row_callbacks::("profile_save_archive", &self.profile_save_archive, event); callbacks.invoke_table_row_callbacks::("profile_wallet_ledger", &self.profile_wallet_ledger, event); + callbacks.invoke_table_row_callbacks::("puzzle_agent_message", &self.puzzle_agent_message, event); + callbacks.invoke_table_row_callbacks::("puzzle_agent_session", &self.puzzle_agent_session, event); + callbacks.invoke_table_row_callbacks::("puzzle_runtime_run", &self.puzzle_runtime_run, event); + callbacks.invoke_table_row_callbacks::("puzzle_work_profile", &self.puzzle_work_profile, event); callbacks.invoke_table_row_callbacks::("quest_log", &self.quest_log, event); callbacks.invoke_table_row_callbacks::("quest_record", &self.quest_record, event); callbacks.invoke_table_row_callbacks::("runtime_setting", &self.runtime_setting, event); @@ -1871,6 +2119,10 @@ fn register_tables(client_cache: &mut __sdk::ClientCache) { asset_entity_binding_table::register_table(client_cache); asset_object_table::register_table(client_cache); battle_state_table::register_table(client_cache); + big_fish_agent_message_table::register_table(client_cache); + big_fish_asset_slot_table::register_table(client_cache); + big_fish_creation_session_table::register_table(client_cache); + big_fish_runtime_run_table::register_table(client_cache); chapter_progression_table::register_table(client_cache); custom_world_agent_message_table::register_table(client_cache); custom_world_agent_operation_table::register_table(client_cache); @@ -1886,6 +2138,10 @@ fn register_tables(client_cache: &mut __sdk::ClientCache) { profile_played_world_table::register_table(client_cache); profile_save_archive_table::register_table(client_cache); profile_wallet_ledger_table::register_table(client_cache); + puzzle_agent_message_table::register_table(client_cache); + puzzle_agent_session_table::register_table(client_cache); + puzzle_runtime_run_table::register_table(client_cache); + puzzle_work_profile_table::register_table(client_cache); quest_log_table::register_table(client_cache); quest_record_table::register_table(client_cache); runtime_setting_table::register_table(client_cache); @@ -1903,6 +2159,10 @@ const ALL_TABLE_NAMES: &'static [&'static str] = &[ "asset_entity_binding", "asset_object", "battle_state", + "big_fish_agent_message", + "big_fish_asset_slot", + "big_fish_creation_session", + "big_fish_runtime_run", "chapter_progression", "custom_world_agent_message", "custom_world_agent_operation", @@ -1918,6 +2178,10 @@ const ALL_TABLE_NAMES: &'static [&'static str] = &[ "profile_played_world", "profile_save_archive", "profile_wallet_ledger", + "puzzle_agent_message", + "puzzle_agent_session", + "puzzle_runtime_run", + "puzzle_work_profile", "quest_log", "quest_record", "runtime_setting", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs new file mode 100644 index 00000000..6a2bfdd8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +use super::big_fish_publish_input_type::BigFishPublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct PublishBigFishGameArgs { + pub input: BigFishPublishInput, +} + + +impl __sdk::InModule for PublishBigFishGameArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_big_fish_game`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_big_fish_game { + fn publish_big_fish_game(&self, input: BigFishPublishInput, +) { + self.publish_big_fish_game_then(input, |_, _| {}); + } + + fn publish_big_fish_game_then( + &self, + input: BigFishPublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl publish_big_fish_game for super::RemoteProcedures { + fn publish_big_fish_game_then( + &self, + input: BigFishPublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "publish_big_fish_game", + PublishBigFishGameArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs new file mode 100644 index 00000000..4592d939 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; +use super::puzzle_publish_input_type::PuzzlePublishInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct PublishPuzzleWorkArgs { + pub input: PuzzlePublishInput, +} + + +impl __sdk::InModule for PublishPuzzleWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `publish_puzzle_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait publish_puzzle_work { + fn publish_puzzle_work(&self, input: PuzzlePublishInput, +) { + self.publish_puzzle_work_then(input, |_, _| {}); + } + + fn publish_puzzle_work_then( + &self, + input: PuzzlePublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl publish_puzzle_work for super::RemoteProcedures { + fn publish_puzzle_work_then( + &self, + input: PuzzlePublishInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( + "publish_puzzle_work", + PublishPuzzleWorkArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_kind_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_kind_type.rs new file mode 100644 index 00000000..56f8df2d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_kind_type.rs @@ -0,0 +1,31 @@ +// 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 PuzzleAgentMessageKind { + Chat, + + Summary, + + ActionResult, + + Warning, + +} + + + +impl __sdk::InModule for PuzzleAgentMessageKind { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_role_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_role_type.rs new file mode 100644 index 00000000..b34f41c4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_role_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, +}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum PuzzleAgentMessageRole { + User, + + Assistant, + + System, + +} + + + +impl __sdk::InModule for PuzzleAgentMessageRole { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_row_type.rs new file mode 100644 index 00000000..ceb74765 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_row_type.rs @@ -0,0 +1,79 @@ +// 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::puzzle_agent_message_role_type::PuzzleAgentMessageRole; +use super::puzzle_agent_message_kind_type::PuzzleAgentMessageKind; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentMessageRow { + pub message_id: String, + pub session_id: String, + pub role: PuzzleAgentMessageRole, + pub kind: PuzzleAgentMessageKind, + pub text: String, + pub created_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for PuzzleAgentMessageRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `PuzzleAgentMessageRow`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleAgentMessageRowCols { + pub message_id: __sdk::__query_builder::Col, + pub session_id: __sdk::__query_builder::Col, + pub role: __sdk::__query_builder::Col, + pub kind: __sdk::__query_builder::Col, + pub text: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for PuzzleAgentMessageRow { + type Cols = PuzzleAgentMessageRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleAgentMessageRowCols { + message_id: __sdk::__query_builder::Col::new(table_name, "message_id"), + session_id: __sdk::__query_builder::Col::new(table_name, "session_id"), + role: __sdk::__query_builder::Col::new(table_name, "role"), + kind: __sdk::__query_builder::Col::new(table_name, "kind"), + text: __sdk::__query_builder::Col::new(table_name, "text"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + + } + } +} + +/// Indexed column accessor struct for the table `PuzzleAgentMessageRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PuzzleAgentMessageRowIxCols { + pub message_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PuzzleAgentMessageRow { + type IxCols = PuzzleAgentMessageRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PuzzleAgentMessageRowIxCols { + message_id: __sdk::__query_builder::IxCol::new(table_name, "message_id"), + session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PuzzleAgentMessageRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_submit_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_submit_input_type.rs new file mode 100644 index 00000000..3a9fa169 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_submit_input_type.rs @@ -0,0 +1,27 @@ +// 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 PuzzleAgentMessageSubmitInput { + pub session_id: String, + pub owner_user_id: String, + pub user_message_id: String, + pub user_message_text: String, + pub submitted_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleAgentMessageSubmitInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_table.rs new file mode 100644 index 00000000..e282d9e9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_table.rs @@ -0,0 +1,165 @@ +// 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::puzzle_agent_message_row_type::PuzzleAgentMessageRow; +use super::puzzle_agent_message_role_type::PuzzleAgentMessageRole; +use super::puzzle_agent_message_kind_type::PuzzleAgentMessageKind; + +/// Table handle for the table `puzzle_agent_message`. +/// +/// Obtain a handle from the [`PuzzleAgentMessageTableAccess::puzzle_agent_message`] method on [`super::RemoteTables`], +/// like `ctx.db.puzzle_agent_message()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.puzzle_agent_message().on_insert(...)`. +pub struct PuzzleAgentMessageTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `puzzle_agent_message`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PuzzleAgentMessageTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PuzzleAgentMessageTableHandle`], which mediates access to the table `puzzle_agent_message`. + fn puzzle_agent_message(&self) -> PuzzleAgentMessageTableHandle<'_>; +} + +impl PuzzleAgentMessageTableAccess for super::RemoteTables { + fn puzzle_agent_message(&self) -> PuzzleAgentMessageTableHandle<'_> { + PuzzleAgentMessageTableHandle { + imp: self.imp.get_table::("puzzle_agent_message"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PuzzleAgentMessageInsertCallbackId(__sdk::CallbackId); +pub struct PuzzleAgentMessageDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PuzzleAgentMessageTableHandle<'ctx> { + type Row = PuzzleAgentMessageRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = PuzzleAgentMessageInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleAgentMessageInsertCallbackId { + PuzzleAgentMessageInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PuzzleAgentMessageInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PuzzleAgentMessageDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleAgentMessageDeleteCallbackId { + PuzzleAgentMessageDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PuzzleAgentMessageDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PuzzleAgentMessageUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PuzzleAgentMessageTableHandle<'ctx> { + type UpdateCallbackId = PuzzleAgentMessageUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PuzzleAgentMessageUpdateCallbackId { + PuzzleAgentMessageUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PuzzleAgentMessageUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `message_id` unique index on the table `puzzle_agent_message`, + /// which allows point queries on the field of the same name + /// via the [`PuzzleAgentMessageMessageIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.puzzle_agent_message().message_id().find(...)`. + pub struct PuzzleAgentMessageMessageIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> PuzzleAgentMessageTableHandle<'ctx> { + /// Get a handle on the `message_id` unique index on the table `puzzle_agent_message`. + pub fn message_id(&self) -> PuzzleAgentMessageMessageIdUnique<'ctx> { + PuzzleAgentMessageMessageIdUnique { + imp: self.imp.get_unique_constraint::("message_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> PuzzleAgentMessageMessageIdUnique<'ctx> { + /// Find the subscribed row whose `message_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::("puzzle_agent_message"); + _table.add_unique_constraint::("message_id", |row| &row.message_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 `PuzzleAgentMessageRow`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait puzzle_agent_messageQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PuzzleAgentMessageRow`. + fn puzzle_agent_message(&self) -> __sdk::__query_builder::Table; + } + + impl puzzle_agent_messageQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_agent_message(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_agent_message") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_create_input_type.rs new file mode 100644 index 00000000..f07fcd6b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_create_input_type.rs @@ -0,0 +1,28 @@ +// 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 PuzzleAgentSessionCreateInput { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub welcome_message_id: String, + pub welcome_message_text: String, + pub created_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleAgentSessionCreateInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_get_input_type.rs new file mode 100644 index 00000000..e411bead --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_get_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 PuzzleAgentSessionGetInput { + pub session_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for PuzzleAgentSessionGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs new file mode 100644 index 00000000..f19f290d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentSessionProcedureResult { + pub ok: bool, + pub session_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for PuzzleAgentSessionProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_row_type.rs new file mode 100644 index 00000000..96e98e56 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_row_type.rs @@ -0,0 +1,96 @@ +// 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::puzzle_agent_stage_type::PuzzleAgentStage; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentSessionRow { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: PuzzleAgentStage, + pub anchor_pack_json: String, + pub draft_json: Option::, + pub last_assistant_reply: Option::, + pub published_profile_id: Option::, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for PuzzleAgentSessionRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `PuzzleAgentSessionRow`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleAgentSessionRowCols { + 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 anchor_pack_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 PuzzleAgentSessionRow { + type Cols = PuzzleAgentSessionRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleAgentSessionRowCols { + 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"), + anchor_pack_json: __sdk::__query_builder::Col::new(table_name, "anchor_pack_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 `PuzzleAgentSessionRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PuzzleAgentSessionRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub session_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PuzzleAgentSessionRow { + type IxCols = PuzzleAgentSessionRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PuzzleAgentSessionRowIxCols { + 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 PuzzleAgentSessionRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_table.rs new file mode 100644 index 00000000..0272d915 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_table.rs @@ -0,0 +1,164 @@ +// 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::puzzle_agent_session_row_type::PuzzleAgentSessionRow; +use super::puzzle_agent_stage_type::PuzzleAgentStage; + +/// Table handle for the table `puzzle_agent_session`. +/// +/// Obtain a handle from the [`PuzzleAgentSessionTableAccess::puzzle_agent_session`] method on [`super::RemoteTables`], +/// like `ctx.db.puzzle_agent_session()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.puzzle_agent_session().on_insert(...)`. +pub struct PuzzleAgentSessionTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `puzzle_agent_session`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PuzzleAgentSessionTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PuzzleAgentSessionTableHandle`], which mediates access to the table `puzzle_agent_session`. + fn puzzle_agent_session(&self) -> PuzzleAgentSessionTableHandle<'_>; +} + +impl PuzzleAgentSessionTableAccess for super::RemoteTables { + fn puzzle_agent_session(&self) -> PuzzleAgentSessionTableHandle<'_> { + PuzzleAgentSessionTableHandle { + imp: self.imp.get_table::("puzzle_agent_session"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PuzzleAgentSessionInsertCallbackId(__sdk::CallbackId); +pub struct PuzzleAgentSessionDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PuzzleAgentSessionTableHandle<'ctx> { + type Row = PuzzleAgentSessionRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = PuzzleAgentSessionInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleAgentSessionInsertCallbackId { + PuzzleAgentSessionInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PuzzleAgentSessionInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PuzzleAgentSessionDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleAgentSessionDeleteCallbackId { + PuzzleAgentSessionDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PuzzleAgentSessionDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PuzzleAgentSessionUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PuzzleAgentSessionTableHandle<'ctx> { + type UpdateCallbackId = PuzzleAgentSessionUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PuzzleAgentSessionUpdateCallbackId { + PuzzleAgentSessionUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PuzzleAgentSessionUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `session_id` unique index on the table `puzzle_agent_session`, + /// which allows point queries on the field of the same name + /// via the [`PuzzleAgentSessionSessionIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.puzzle_agent_session().session_id().find(...)`. + pub struct PuzzleAgentSessionSessionIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> PuzzleAgentSessionTableHandle<'ctx> { + /// Get a handle on the `session_id` unique index on the table `puzzle_agent_session`. + pub fn session_id(&self) -> PuzzleAgentSessionSessionIdUnique<'ctx> { + PuzzleAgentSessionSessionIdUnique { + imp: self.imp.get_unique_constraint::("session_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> PuzzleAgentSessionSessionIdUnique<'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::("puzzle_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 `PuzzleAgentSessionRow`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait puzzle_agent_sessionQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PuzzleAgentSessionRow`. + fn puzzle_agent_session(&self) -> __sdk::__query_builder::Table; + } + + impl puzzle_agent_sessionQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_agent_session(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_agent_session") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_stage_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_stage_type.rs new file mode 100644 index 00000000..00cbc81b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_stage_type.rs @@ -0,0 +1,33 @@ +// 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 PuzzleAgentStage { + CollectingAnchors, + + DraftReady, + + ImageRefining, + + ReadyToPublish, + + Published, + +} + + + +impl __sdk::InModule for PuzzleAgentStage { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_compile_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_compile_input_type.rs new file mode 100644 index 00000000..703538e4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_compile_input_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleDraftCompileInput { + pub session_id: String, + pub owner_user_id: String, + pub compiled_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleDraftCompileInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_images_save_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_images_save_input_type.rs new file mode 100644 index 00000000..0c2196b4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_images_save_input_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleGeneratedImagesSaveInput { + pub session_id: String, + pub owner_user_id: String, + pub candidates_json: String, + pub saved_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleGeneratedImagesSaveInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publication_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publication_status_type.rs new file mode 100644 index 00000000..7ff8b5c6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publication_status_type.rs @@ -0,0 +1,27 @@ +// 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 PuzzlePublicationStatus { + Draft, + + Published, + +} + + + +impl __sdk::InModule for PuzzlePublicationStatus { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publish_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publish_input_type.rs new file mode 100644 index 00000000..d3a5d26d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_publish_input_type.rs @@ -0,0 +1,31 @@ +// 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 PuzzlePublishInput { + pub session_id: String, + pub owner_user_id: String, + pub work_id: String, + pub profile_id: String, + pub author_display_name: String, + pub level_name: Option::, + pub summary: Option::, + pub theme_tags: Option::>, + pub published_at_micros: i64, +} + + +impl __sdk::InModule for PuzzlePublishInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_drag_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_drag_input_type.rs new file mode 100644 index 00000000..753e2932 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_drag_input_type.rs @@ -0,0 +1,28 @@ +// 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 PuzzleRunDragInput { + pub run_id: String, + pub owner_user_id: String, + pub piece_id: String, + pub target_row: u32, + pub target_col: u32, + pub dragged_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleRunDragInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_get_input_type.rs new file mode 100644 index 00000000..cf15f8b3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_get_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 PuzzleRunGetInput { + pub run_id: String, + pub owner_user_id: String, +} + + +impl __sdk::InModule for PuzzleRunGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_type.rs new file mode 100644 index 00000000..d6a9a285 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_next_level_input_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRunNextLevelInput { + pub run_id: String, + pub owner_user_id: String, + pub advanced_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleRunNextLevelInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs new file mode 100644 index 00000000..215490dc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRunProcedureResult { + pub ok: bool, + pub run_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for PuzzleRunProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_start_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_start_input_type.rs new file mode 100644 index 00000000..55e0a93e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_start_input_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRunStartInput { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub started_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleRunStartInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_swap_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_swap_input_type.rs new file mode 100644 index 00000000..3b5baa18 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_swap_input_type.rs @@ -0,0 +1,27 @@ +// 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 PuzzleRunSwapInput { + pub run_id: String, + pub owner_user_id: String, + pub first_piece_id: String, + pub second_piece_id: String, + pub swapped_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleRunSwapInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_row_type.rs new file mode 100644 index 00000000..daf67f5b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_row_type.rs @@ -0,0 +1,95 @@ +// 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 PuzzleRuntimeRunRow { + pub run_id: String, + pub owner_user_id: String, + pub entry_profile_id: String, + pub current_profile_id: String, + pub cleared_level_count: u32, + pub current_level_index: u32, + pub current_grid_size: u32, + pub played_profile_ids_json: String, + pub previous_level_tags_json: String, + pub snapshot_json: String, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, +} + + +impl __sdk::InModule for PuzzleRuntimeRunRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `PuzzleRuntimeRunRow`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleRuntimeRunRowCols { + pub run_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub entry_profile_id: __sdk::__query_builder::Col, + pub current_profile_id: __sdk::__query_builder::Col, + pub cleared_level_count: __sdk::__query_builder::Col, + pub current_level_index: __sdk::__query_builder::Col, + pub current_grid_size: __sdk::__query_builder::Col, + pub played_profile_ids_json: __sdk::__query_builder::Col, + pub previous_level_tags_json: __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 PuzzleRuntimeRunRow { + type Cols = PuzzleRuntimeRunRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleRuntimeRunRowCols { + run_id: __sdk::__query_builder::Col::new(table_name, "run_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + entry_profile_id: __sdk::__query_builder::Col::new(table_name, "entry_profile_id"), + current_profile_id: __sdk::__query_builder::Col::new(table_name, "current_profile_id"), + cleared_level_count: __sdk::__query_builder::Col::new(table_name, "cleared_level_count"), + current_level_index: __sdk::__query_builder::Col::new(table_name, "current_level_index"), + current_grid_size: __sdk::__query_builder::Col::new(table_name, "current_grid_size"), + played_profile_ids_json: __sdk::__query_builder::Col::new(table_name, "played_profile_ids_json"), + previous_level_tags_json: __sdk::__query_builder::Col::new(table_name, "previous_level_tags_json"), + 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 `PuzzleRuntimeRunRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PuzzleRuntimeRunRowIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub run_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for PuzzleRuntimeRunRow { + type IxCols = PuzzleRuntimeRunRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PuzzleRuntimeRunRowIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"), + + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for PuzzleRuntimeRunRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_table.rs new file mode 100644 index 00000000..5447392d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_run_table.rs @@ -0,0 +1,163 @@ +// 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::puzzle_runtime_run_row_type::PuzzleRuntimeRunRow; + +/// Table handle for the table `puzzle_runtime_run`. +/// +/// Obtain a handle from the [`PuzzleRuntimeRunTableAccess::puzzle_runtime_run`] method on [`super::RemoteTables`], +/// like `ctx.db.puzzle_runtime_run()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.puzzle_runtime_run().on_insert(...)`. +pub struct PuzzleRuntimeRunTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `puzzle_runtime_run`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PuzzleRuntimeRunTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PuzzleRuntimeRunTableHandle`], which mediates access to the table `puzzle_runtime_run`. + fn puzzle_runtime_run(&self) -> PuzzleRuntimeRunTableHandle<'_>; +} + +impl PuzzleRuntimeRunTableAccess for super::RemoteTables { + fn puzzle_runtime_run(&self) -> PuzzleRuntimeRunTableHandle<'_> { + PuzzleRuntimeRunTableHandle { + imp: self.imp.get_table::("puzzle_runtime_run"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PuzzleRuntimeRunInsertCallbackId(__sdk::CallbackId); +pub struct PuzzleRuntimeRunDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PuzzleRuntimeRunTableHandle<'ctx> { + type Row = PuzzleRuntimeRunRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = PuzzleRuntimeRunInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleRuntimeRunInsertCallbackId { + PuzzleRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PuzzleRuntimeRunInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PuzzleRuntimeRunDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleRuntimeRunDeleteCallbackId { + PuzzleRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PuzzleRuntimeRunDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PuzzleRuntimeRunUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PuzzleRuntimeRunTableHandle<'ctx> { + type UpdateCallbackId = PuzzleRuntimeRunUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PuzzleRuntimeRunUpdateCallbackId { + PuzzleRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PuzzleRuntimeRunUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `run_id` unique index on the table `puzzle_runtime_run`, + /// which allows point queries on the field of the same name + /// via the [`PuzzleRuntimeRunRunIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.puzzle_runtime_run().run_id().find(...)`. + pub struct PuzzleRuntimeRunRunIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> PuzzleRuntimeRunTableHandle<'ctx> { + /// Get a handle on the `run_id` unique index on the table `puzzle_runtime_run`. + pub fn run_id(&self) -> PuzzleRuntimeRunRunIdUnique<'ctx> { + PuzzleRuntimeRunRunIdUnique { + imp: self.imp.get_unique_constraint::("run_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> PuzzleRuntimeRunRunIdUnique<'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::("puzzle_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 `PuzzleRuntimeRunRow`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait puzzle_runtime_runQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PuzzleRuntimeRunRow`. + fn puzzle_runtime_run(&self) -> __sdk::__query_builder::Table; + } + + impl puzzle_runtime_runQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_runtime_run(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_runtime_run") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_select_cover_image_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_select_cover_image_input_type.rs new file mode 100644 index 00000000..847ac3a4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_select_cover_image_input_type.rs @@ -0,0 +1,26 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleSelectCoverImageInput { + pub session_id: String, + pub owner_user_id: String, + pub candidate_id: String, + pub selected_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleSelectCoverImageInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_get_input_type.rs new file mode 100644 index 00000000..74cf57a1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_get_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 PuzzleWorkGetInput { + pub profile_id: String, +} + + +impl __sdk::InModule for PuzzleWorkGetInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs new file mode 100644 index 00000000..0da3b49c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleWorkProcedureResult { + pub ok: bool, + pub item_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for PuzzleWorkProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs new file mode 100644 index 00000000..e5482282 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_row_type.rs @@ -0,0 +1,113 @@ +// 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::puzzle_publication_status_type::PuzzlePublicationStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleWorkProfileRow { + pub profile_id: String, + pub work_id: String, + pub owner_user_id: String, + pub source_session_id: Option::, + pub author_display_name: String, + pub level_name: String, + pub summary: String, + pub theme_tags_json: String, + pub cover_image_src: Option::, + pub cover_asset_id: Option::, + pub publication_status: PuzzlePublicationStatus, + pub play_count: u32, + pub anchor_pack_json: String, + pub publish_ready: bool, + pub created_at: __sdk::Timestamp, + pub updated_at: __sdk::Timestamp, + pub published_at: Option::<__sdk::Timestamp>, +} + + +impl __sdk::InModule for PuzzleWorkProfileRow { + type Module = super::RemoteModule; +} + + +/// Column accessor struct for the table `PuzzleWorkProfileRow`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleWorkProfileRowCols { + 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 level_name: __sdk::__query_builder::Col, + pub summary: __sdk::__query_builder::Col, + pub theme_tags_json: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col>, + pub cover_asset_id: __sdk::__query_builder::Col>, + pub publication_status: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub anchor_pack_json: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, + pub published_at: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow { + type Cols = PuzzleWorkProfileRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleWorkProfileRowCols { + 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"), + level_name: __sdk::__query_builder::Col::new(table_name, "level_name"), + summary: __sdk::__query_builder::Col::new(table_name, "summary"), + theme_tags_json: __sdk::__query_builder::Col::new(table_name, "theme_tags_json"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + anchor_pack_json: __sdk::__query_builder::Col::new(table_name, "anchor_pack_json"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), + + } + } +} + +/// Indexed column accessor struct for the table `PuzzleWorkProfileRow`. +/// +/// Provides typed access to indexed columns for query building. +pub struct PuzzleWorkProfileRowIxCols { + 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 PuzzleWorkProfileRow { + type IxCols = PuzzleWorkProfileRowIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + PuzzleWorkProfileRowIxCols { + 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 PuzzleWorkProfileRow {} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_table.rs new file mode 100644 index 00000000..c5f14f0f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_table.rs @@ -0,0 +1,164 @@ +// 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::puzzle_work_profile_row_type::PuzzleWorkProfileRow; +use super::puzzle_publication_status_type::PuzzlePublicationStatus; + +/// Table handle for the table `puzzle_work_profile`. +/// +/// Obtain a handle from the [`PuzzleWorkProfileTableAccess::puzzle_work_profile`] method on [`super::RemoteTables`], +/// like `ctx.db.puzzle_work_profile()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.puzzle_work_profile().on_insert(...)`. +pub struct PuzzleWorkProfileTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `puzzle_work_profile`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PuzzleWorkProfileTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PuzzleWorkProfileTableHandle`], which mediates access to the table `puzzle_work_profile`. + fn puzzle_work_profile(&self) -> PuzzleWorkProfileTableHandle<'_>; +} + +impl PuzzleWorkProfileTableAccess for super::RemoteTables { + fn puzzle_work_profile(&self) -> PuzzleWorkProfileTableHandle<'_> { + PuzzleWorkProfileTableHandle { + imp: self.imp.get_table::("puzzle_work_profile"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PuzzleWorkProfileInsertCallbackId(__sdk::CallbackId); +pub struct PuzzleWorkProfileDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PuzzleWorkProfileTableHandle<'ctx> { + type Row = PuzzleWorkProfileRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = PuzzleWorkProfileInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleWorkProfileInsertCallbackId { + PuzzleWorkProfileInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PuzzleWorkProfileInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PuzzleWorkProfileDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleWorkProfileDeleteCallbackId { + PuzzleWorkProfileDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PuzzleWorkProfileDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct PuzzleWorkProfileUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for PuzzleWorkProfileTableHandle<'ctx> { + type UpdateCallbackId = PuzzleWorkProfileUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> PuzzleWorkProfileUpdateCallbackId { + PuzzleWorkProfileUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: PuzzleWorkProfileUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + + /// Access to the `profile_id` unique index on the table `puzzle_work_profile`, + /// which allows point queries on the field of the same name + /// via the [`PuzzleWorkProfileProfileIdUnique::find`] method. + /// + /// Users are encouraged not to explicitly reference this type, + /// but to directly chain method calls, + /// like `ctx.db.puzzle_work_profile().profile_id().find(...)`. + pub struct PuzzleWorkProfileProfileIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, + } + + impl<'ctx> PuzzleWorkProfileTableHandle<'ctx> { + /// Get a handle on the `profile_id` unique index on the table `puzzle_work_profile`. + pub fn profile_id(&self) -> PuzzleWorkProfileProfileIdUnique<'ctx> { + PuzzleWorkProfileProfileIdUnique { + imp: self.imp.get_unique_constraint::("profile_id"), + phantom: std::marker::PhantomData, + } + } + } + + impl<'ctx> PuzzleWorkProfileProfileIdUnique<'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::("puzzle_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 `PuzzleWorkProfileRow`. + /// + /// Implemented for [`__sdk::QueryTableAccessor`]. + pub trait puzzle_work_profileQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PuzzleWorkProfileRow`. + fn puzzle_work_profile(&self) -> __sdk::__query_builder::Table; + } + + impl puzzle_work_profileQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_work_profile(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_work_profile") + } + } + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_upsert_input_type.rs new file mode 100644 index 00000000..ae412d68 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_upsert_input_type.rs @@ -0,0 +1,30 @@ +// 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 PuzzleWorkUpsertInput { + pub profile_id: String, + pub owner_user_id: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec::, + pub cover_image_src: Option::, + pub cover_asset_id: Option::, + pub updated_at_micros: i64, +} + + +impl __sdk::InModule for PuzzleWorkUpsertInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_list_input_type.rs new file mode 100644 index 00000000..e1d55a5f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_list_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 PuzzleWorksListInput { + pub owner_user_id: String, +} + + +impl __sdk::InModule for PuzzleWorksListInput { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs new file mode 100644 index 00000000..80a1c401 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_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, +}; + + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleWorksProcedureResult { + pub ok: bool, + pub items_json: Option::, + pub error_message: Option::, +} + + +impl __sdk::InModule for PuzzleWorksProcedureResult { + type Module = super::RemoteModule; +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs new file mode 100644 index 00000000..15ccc81f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +use super::puzzle_generated_images_save_input_type::PuzzleGeneratedImagesSaveInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SavePuzzleGeneratedImagesArgs { + pub input: PuzzleGeneratedImagesSaveInput, +} + + +impl __sdk::InModule for SavePuzzleGeneratedImagesArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `save_puzzle_generated_images`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait save_puzzle_generated_images { + fn save_puzzle_generated_images(&self, input: PuzzleGeneratedImagesSaveInput, +) { + self.save_puzzle_generated_images_then(input, |_, _| {}); + } + + fn save_puzzle_generated_images_then( + &self, + input: PuzzleGeneratedImagesSaveInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl save_puzzle_generated_images for super::RemoteProcedures { + fn save_puzzle_generated_images_then( + &self, + input: PuzzleGeneratedImagesSaveInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "save_puzzle_generated_images", + SavePuzzleGeneratedImagesArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs new file mode 100644 index 00000000..b12bb89a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +use super::puzzle_select_cover_image_input_type::PuzzleSelectCoverImageInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SelectPuzzleCoverImageArgs { + pub input: PuzzleSelectCoverImageInput, +} + + +impl __sdk::InModule for SelectPuzzleCoverImageArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `select_puzzle_cover_image`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait select_puzzle_cover_image { + fn select_puzzle_cover_image(&self, input: PuzzleSelectCoverImageInput, +) { + self.select_puzzle_cover_image_then(input, |_, _| {}); + } + + fn select_puzzle_cover_image_then( + &self, + input: PuzzleSelectCoverImageInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl select_puzzle_cover_image for super::RemoteProcedures { + fn select_puzzle_cover_image_then( + &self, + input: PuzzleSelectCoverImageInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "select_puzzle_cover_image", + SelectPuzzleCoverImageArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_big_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_big_fish_run_procedure.rs new file mode 100644 index 00000000..b9b3b28c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_big_fish_run_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_run_procedure_result_type::BigFishRunProcedureResult; +use super::big_fish_run_start_input_type::BigFishRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct StartBigFishRunArgs { + pub input: BigFishRunStartInput, +} + + +impl __sdk::InModule for StartBigFishRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_big_fish_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_big_fish_run { + fn start_big_fish_run(&self, input: BigFishRunStartInput, +) { + self.start_big_fish_run_then(input, |_, _| {}); + } + + fn start_big_fish_run_then( + &self, + input: BigFishRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl start_big_fish_run for super::RemoteProcedures { + fn start_big_fish_run_then( + &self, + input: BigFishRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishRunProcedureResult>( + "start_big_fish_run", + StartBigFishRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs new file mode 100644 index 00000000..849856c8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; +use super::puzzle_run_start_input_type::PuzzleRunStartInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct StartPuzzleRunArgs { + pub input: PuzzleRunStartInput, +} + + +impl __sdk::InModule for StartPuzzleRunArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `start_puzzle_run`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait start_puzzle_run { + fn start_puzzle_run(&self, input: PuzzleRunStartInput, +) { + self.start_puzzle_run_then(input, |_, _| {}); + } + + fn start_puzzle_run_then( + &self, + input: PuzzleRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl start_puzzle_run for super::RemoteProcedures { + fn start_puzzle_run_then( + &self, + input: PuzzleRunStartInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( + "start_puzzle_run", + StartPuzzleRunArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_input_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_input_procedure.rs new file mode 100644 index 00000000..b6e481b4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_input_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_run_procedure_result_type::BigFishRunProcedureResult; +use super::big_fish_run_input_submit_input_type::BigFishRunInputSubmitInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SubmitBigFishInputArgs { + pub input: BigFishRunInputSubmitInput, +} + + +impl __sdk::InModule for SubmitBigFishInputArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `submit_big_fish_input`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait submit_big_fish_input { + fn submit_big_fish_input(&self, input: BigFishRunInputSubmitInput, +) { + self.submit_big_fish_input_then(input, |_, _| {}); + } + + fn submit_big_fish_input_then( + &self, + input: BigFishRunInputSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl submit_big_fish_input for super::RemoteProcedures { + fn submit_big_fish_input_then( + &self, + input: BigFishRunInputSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishRunProcedureResult>( + "submit_big_fish_input", + SubmitBigFishInputArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs new file mode 100644 index 00000000..0fd8dc0d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs @@ -0,0 +1,58 @@ +// 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::big_fish_session_procedure_result_type::BigFishSessionProcedureResult; +use super::big_fish_message_submit_input_type::BigFishMessageSubmitInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SubmitBigFishMessageArgs { + pub input: BigFishMessageSubmitInput, +} + + +impl __sdk::InModule for SubmitBigFishMessageArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `submit_big_fish_message`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait submit_big_fish_message { + fn submit_big_fish_message(&self, input: BigFishMessageSubmitInput, +) { + self.submit_big_fish_message_then(input, |_, _| {}); + } + + fn submit_big_fish_message_then( + &self, + input: BigFishMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl submit_big_fish_message for super::RemoteProcedures { + fn submit_big_fish_message_then( + &self, + input: BigFishMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>( + "submit_big_fish_message", + SubmitBigFishMessageArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs new file mode 100644 index 00000000..4bca696e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; +use super::puzzle_agent_message_submit_input_type::PuzzleAgentMessageSubmitInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SubmitPuzzleAgentMessageArgs { + pub input: PuzzleAgentMessageSubmitInput, +} + + +impl __sdk::InModule for SubmitPuzzleAgentMessageArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `submit_puzzle_agent_message`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait submit_puzzle_agent_message { + fn submit_puzzle_agent_message(&self, input: PuzzleAgentMessageSubmitInput, +) { + self.submit_puzzle_agent_message_then(input, |_, _| {}); + } + + fn submit_puzzle_agent_message_then( + &self, + input: PuzzleAgentMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl submit_puzzle_agent_message for super::RemoteProcedures { + fn submit_puzzle_agent_message_then( + &self, + input: PuzzleAgentMessageSubmitInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>( + "submit_puzzle_agent_message", + SubmitPuzzleAgentMessageArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs new file mode 100644 index 00000000..dfcb2750 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_run_procedure_result_type::PuzzleRunProcedureResult; +use super::puzzle_run_swap_input_type::PuzzleRunSwapInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct SwapPuzzlePiecesArgs { + pub input: PuzzleRunSwapInput, +} + + +impl __sdk::InModule for SwapPuzzlePiecesArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `swap_puzzle_pieces`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait swap_puzzle_pieces { + fn swap_puzzle_pieces(&self, input: PuzzleRunSwapInput, +) { + self.swap_puzzle_pieces_then(input, |_, _| {}); + } + + fn swap_puzzle_pieces_then( + &self, + input: PuzzleRunSwapInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl swap_puzzle_pieces for super::RemoteProcedures { + fn swap_puzzle_pieces_then( + &self, + input: PuzzleRunSwapInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( + "swap_puzzle_pieces", + SwapPuzzlePiecesArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs new file mode 100644 index 00000000..14a134df --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs @@ -0,0 +1,58 @@ +// 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::puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; +use super::puzzle_work_upsert_input_type::PuzzleWorkUpsertInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] + struct UpdatePuzzleWorkArgs { + pub input: PuzzleWorkUpsertInput, +} + + +impl __sdk::InModule for UpdatePuzzleWorkArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_puzzle_work`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_puzzle_work { + fn update_puzzle_work(&self, input: PuzzleWorkUpsertInput, +) { + self.update_puzzle_work_then(input, |_, _| {}); + } + + fn update_puzzle_work_then( + &self, + input: PuzzleWorkUpsertInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ); +} + +impl update_puzzle_work for super::RemoteProcedures { + fn update_puzzle_work_then( + &self, + input: PuzzleWorkUpsertInput, + + __callback: impl FnOnce(&super::ProcedureEventContext, Result) + Send + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( + "update_puzzle_work", + UpdatePuzzleWorkArgs { input, }, + __callback, + ); + } +} + diff --git a/server-rs/crates/spacetime-module/Cargo.toml b/server-rs/crates/spacetime-module/Cargo.toml index 4df87135..cc1f9adb 100644 --- a/server-rs/crates/spacetime-module/Cargo.toml +++ b/server-rs/crates/spacetime-module/Cargo.toml @@ -9,13 +9,16 @@ crate-type = ["cdylib"] [dependencies] log = { workspace = true } +serde = { version = "1", features = ["derive"] } serde_json = "1" module-ai = { path = "../module-ai", default-features = false, features = ["spacetime-types"] } module-assets = { path = "../module-assets", default-features = false, features = ["spacetime-types"] } +module-big-fish = { path = "../module-big-fish", default-features = false, features = ["spacetime-types"] } module-combat = { path = "../module-combat", default-features = false, features = ["spacetime-types"] } module-inventory = { path = "../module-inventory", default-features = false, features = ["spacetime-types"] } module-custom-world = { path = "../module-custom-world", default-features = false, features = ["spacetime-types"] } module-npc = { path = "../module-npc", default-features = false, features = ["spacetime-types"] } +module-puzzle = { path = "../module-puzzle", default-features = false, features = ["spacetime-types"] } module-progression = { path = "../module-progression", default-features = false, features = ["spacetime-types"] } module-quest = { path = "../module-quest", default-features = false, features = ["spacetime-types"] } module-runtime = { path = "../module-runtime", default-features = false, features = ["spacetime-types"] } diff --git a/server-rs/crates/spacetime-module/src/lib.rs b/server-rs/crates/spacetime-module/src/lib.rs index 99f787d2..8d836e54 100644 --- a/server-rs/crates/spacetime-module/src/lib.rs +++ b/server-rs/crates/spacetime-module/src/lib.rs @@ -15,6 +15,21 @@ use module_assets::{ INITIAL_ASSET_OBJECT_VERSION, validate_asset_entity_binding_fields, validate_asset_object_fields, }; +use module_big_fish::{ + BigFishAgentMessageKind, BigFishAgentMessageRole, BigFishAgentMessageSnapshot, + BigFishAssetGenerateInput, BigFishAssetKind, BigFishAssetSlotSnapshot, BigFishAssetStatus, + BigFishCreationStage, BigFishDraftCompileInput, BigFishMessageSubmitInput, BigFishPublishInput, + BigFishRunGetInput, BigFishRunInputSubmitInput, BigFishRunProcedureResult, BigFishRunStartInput, + BigFishRunStatus, BigFishRuntimeSnapshot, BigFishSessionCreateInput, BigFishSessionGetInput, + BigFishSessionProcedureResult, BigFishSessionSnapshot, + advance_runtime_snapshot, build_asset_coverage, build_generated_asset_slot, + build_initial_runtime_snapshot, compile_default_draft, deserialize_anchor_pack, + deserialize_draft, deserialize_runtime_snapshot, empty_anchor_pack, infer_anchor_pack, + serialize_anchor_pack, serialize_asset_coverage, serialize_draft, serialize_runtime_snapshot, + validate_asset_generate_input, validate_draft_compile_input, validate_message_submit_input, + validate_publish_input, validate_run_get_input, validate_run_input_submit_input, + validate_run_start_input, validate_session_create_input, validate_session_get_input, +}; use module_combat::{ BATTLE_STATE_ID_PREFIX, BattleMode, BattleStateInput, BattleStateProcedureResult, BattleStateQueryInput, BattleStateSnapshot, BattleStatus, CombatOutcome, @@ -131,6 +146,8 @@ use shared_kernel::format_timestamp_micros; use serde_json::{Map as JsonMap, Value as JsonValue, json}; use spacetimedb::{ProcedureContext, ReducerContext, SpacetimeType, Table, Timestamp}; +mod puzzle; + // 这层输入只服务 NPC 开战编排;普通聊天、援手、招募继续走已有 resolve_npc_interaction 接口。 #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct ResolveNpcBattleInteractionInput { @@ -162,6 +179,77 @@ pub struct NpcBattleInteractionProcedureResult { pub error_message: Option, } +#[spacetimedb::table( + accessor = big_fish_creation_session, + index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct BigFishCreationSession { + #[primary_key] + session_id: String, + owner_user_id: String, + seed_text: String, + current_turn: u32, + progress_percent: u32, + stage: BigFishCreationStage, + anchor_pack_json: String, + draft_json: Option, + asset_coverage_json: String, + last_assistant_reply: Option, + publish_ready: bool, + created_at: Timestamp, + updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = big_fish_agent_message, + index(accessor = by_big_fish_message_session_id, btree(columns = [session_id])) +)] +pub struct BigFishAgentMessage { + #[primary_key] + message_id: String, + session_id: String, + role: BigFishAgentMessageRole, + kind: BigFishAgentMessageKind, + text: String, + created_at: Timestamp, +} + +#[spacetimedb::table( + accessor = big_fish_asset_slot, + index(accessor = by_big_fish_asset_session_id, btree(columns = [session_id])) +)] +pub struct BigFishAssetSlot { + #[primary_key] + slot_id: String, + session_id: String, + asset_kind: BigFishAssetKind, + level: Option, + motion_key: Option, + status: BigFishAssetStatus, + asset_url: Option, + prompt_snapshot: String, + updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = big_fish_runtime_run, + index(accessor = by_big_fish_run_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_big_fish_run_session_id, btree(columns = [session_id])) +)] +pub struct BigFishRuntimeRun { + #[primary_key] + run_id: String, + session_id: String, + owner_user_id: String, + status: BigFishRunStatus, + snapshot_json: String, + last_input_x: f32, + last_input_y: f32, + tick: u64, + created_at: Timestamp, + updated_at: Timestamp, +} + #[spacetimedb::table( accessor = asset_object, index(accessor = by_bucket_object_key, btree(columns = [bucket, object_key])) @@ -1491,6 +1579,177 @@ pub fn get_story_session_state( } } +#[spacetimedb::procedure] +pub fn create_big_fish_session( + ctx: &mut ProcedureContext, + input: BigFishSessionCreateInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| create_big_fish_session_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_big_fish_session( + ctx: &mut ProcedureContext, + input: BigFishSessionGetInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| get_big_fish_session_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn submit_big_fish_message( + ctx: &mut ProcedureContext, + input: BigFishMessageSubmitInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| submit_big_fish_message_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn compile_big_fish_draft( + ctx: &mut ProcedureContext, + input: BigFishDraftCompileInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| compile_big_fish_draft_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn generate_big_fish_asset( + ctx: &mut ProcedureContext, + input: BigFishAssetGenerateInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| generate_big_fish_asset_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn publish_big_fish_game( + ctx: &mut ProcedureContext, + input: BigFishPublishInput, +) -> BigFishSessionProcedureResult { + match ctx.try_with_tx(|tx| publish_big_fish_game_tx(tx, input.clone())) { + Ok(session) => BigFishSessionProcedureResult { + ok: true, + session: Some(session), + error_message: None, + }, + Err(message) => BigFishSessionProcedureResult { + ok: false, + session: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn start_big_fish_run( + ctx: &mut ProcedureContext, + input: BigFishRunStartInput, +) -> BigFishRunProcedureResult { + match ctx.try_with_tx(|tx| start_big_fish_run_tx(tx, input.clone())) { + Ok(run) => BigFishRunProcedureResult { + ok: true, + run: Some(run), + error_message: None, + }, + Err(message) => BigFishRunProcedureResult { + ok: false, + run: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn submit_big_fish_input( + ctx: &mut ProcedureContext, + input: BigFishRunInputSubmitInput, +) -> BigFishRunProcedureResult { + match ctx.try_with_tx(|tx| submit_big_fish_input_tx(tx, input.clone())) { + Ok(run) => BigFishRunProcedureResult { + ok: true, + run: Some(run), + error_message: None, + }, + Err(message) => BigFishRunProcedureResult { + ok: false, + run: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_big_fish_run( + ctx: &mut ProcedureContext, + input: BigFishRunGetInput, +) -> BigFishRunProcedureResult { + match ctx.try_with_tx(|tx| get_big_fish_run_tx(tx, input.clone())) { + Ok(run) => BigFishRunProcedureResult { + ok: true, + run: Some(run), + error_message: None, + }, + Err(message) => BigFishRunProcedureResult { + ok: false, + run: None, + error_message: Some(message), + }, + } +} + // Stage 6 先把 Agent 会话骨架写入 SpacetimeDB,LLM 采集与卡片生成后续再接入。 #[spacetimedb::procedure] pub fn create_custom_world_agent_session( @@ -1658,6 +1917,465 @@ fn get_story_session_state_tx( Ok((session_snapshot, events)) } +fn create_big_fish_session_tx( + ctx: &ReducerContext, + input: BigFishSessionCreateInput, +) -> Result { + validate_session_create_input(&input).map_err(|error| error.to_string())?; + if ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .is_some() + { + return Err("big_fish_creation_session.session_id 已存在".to_string()); + } + if ctx + .db + .big_fish_agent_message() + .message_id() + .find(&input.welcome_message_id) + .is_some() + { + return Err("big_fish_agent_message.message_id 已存在".to_string()); + } + + let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros); + let anchor_pack = infer_anchor_pack(&input.seed_text, None); + let asset_coverage = build_asset_coverage(None, &[]); + ctx.db + .big_fish_creation_session() + .insert(BigFishCreationSession { + 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: 20, + stage: BigFishCreationStage::CollectingAnchors, + anchor_pack_json: serialize_anchor_pack(&anchor_pack) + .map_err(|error| error.to_string())?, + draft_json: None, + asset_coverage_json: serialize_asset_coverage(&asset_coverage) + .map_err(|error| error.to_string())?, + last_assistant_reply: Some(input.welcome_message_text.clone()), + publish_ready: false, + created_at, + updated_at: created_at, + }); + ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { + message_id: input.welcome_message_id, + session_id: input.session_id.clone(), + role: BigFishAgentMessageRole::Assistant, + kind: BigFishAgentMessageKind::Chat, + text: input.welcome_message_text, + created_at, + }); + + get_big_fish_session_tx( + ctx, + BigFishSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_big_fish_session_tx( + ctx: &ReducerContext, + input: BigFishSessionGetInput, +) -> Result { + validate_session_get_input(&input).map_err(|error| error.to_string())?; + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + + build_big_fish_session_snapshot(ctx, &session) +} + +fn submit_big_fish_message_tx( + ctx: &ReducerContext, + input: BigFishMessageSubmitInput, +) -> Result { + validate_message_submit_input(&input).map_err(|error| error.to_string())?; + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + if ctx + .db + .big_fish_agent_message() + .message_id() + .find(&input.user_message_id) + .is_some() + { + return Err("big_fish_agent_message.user_message_id 已存在".to_string()); + } + if ctx + .db + .big_fish_agent_message() + .message_id() + .find(&input.assistant_message_id) + .is_some() + { + return Err("big_fish_agent_message.assistant_message_id 已存在".to_string()); + } + + let submitted_at = Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros); + ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { + message_id: input.user_message_id, + session_id: input.session_id.clone(), + role: BigFishAgentMessageRole::User, + kind: BigFishAgentMessageKind::Chat, + text: input.user_message_text.trim().to_string(), + created_at: submitted_at, + }); + + let anchor_pack = infer_anchor_pack(&session.seed_text, Some(&input.user_message_text)); + let assistant_text = "我已经把这版方向收束成 4 个高杠杆锚点,可以继续细化,也可以直接编译第一版玩法草稿。" + .to_string(); + ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { + message_id: input.assistant_message_id, + session_id: input.session_id.clone(), + role: BigFishAgentMessageRole::Assistant, + kind: BigFishAgentMessageKind::Summary, + text: assistant_text.clone(), + created_at: submitted_at, + }); + + let next_session = BigFishCreationSession { + session_id: session.session_id.clone(), + owner_user_id: session.owner_user_id.clone(), + seed_text: session.seed_text.clone(), + current_turn: session.current_turn.saturating_add(1), + progress_percent: 60, + stage: BigFishCreationStage::CollectingAnchors, + anchor_pack_json: serialize_anchor_pack(&anchor_pack).map_err(|error| error.to_string())?, + draft_json: session.draft_json.clone(), + asset_coverage_json: session.asset_coverage_json.clone(), + last_assistant_reply: Some(assistant_text), + publish_ready: session.publish_ready, + created_at: session.created_at, + updated_at: submitted_at, + }; + replace_big_fish_session(ctx, &session, next_session); + + get_big_fish_session_tx( + ctx, + BigFishSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn compile_big_fish_draft_tx( + ctx: &ReducerContext, + input: BigFishDraftCompileInput, +) -> Result { + validate_draft_compile_input(&input).map_err(|error| error.to_string())?; + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + let anchor_pack = + deserialize_anchor_pack(&session.anchor_pack_json).map_err(|error| error.to_string())?; + let draft = compile_default_draft(&anchor_pack); + let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id); + let coverage = build_asset_coverage(Some(&draft), &asset_slots); + let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + let reply = "第一版玩法草稿已编译完成,可以在结果页逐级生成主图、动作和场地背景。".to_string(); + + let next_session = BigFishCreationSession { + session_id: session.session_id.clone(), + owner_user_id: session.owner_user_id.clone(), + seed_text: session.seed_text.clone(), + current_turn: session.current_turn, + progress_percent: 80, + stage: BigFishCreationStage::DraftReady, + anchor_pack_json: session.anchor_pack_json.clone(), + draft_json: Some(serialize_draft(&draft).map_err(|error| error.to_string())?), + asset_coverage_json: serialize_asset_coverage(&coverage) + .map_err(|error| error.to_string())?, + last_assistant_reply: Some(reply.clone()), + publish_ready: coverage.publish_ready, + created_at: session.created_at, + updated_at: compiled_at, + }; + replace_big_fish_session(ctx, &session, next_session); + append_big_fish_system_message( + ctx, + &input.session_id, + format!("big-fish-message-compile-{}", input.compiled_at_micros), + reply, + input.compiled_at_micros, + ); + + get_big_fish_session_tx( + ctx, + BigFishSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn generate_big_fish_asset_tx( + ctx: &ReducerContext, + input: BigFishAssetGenerateInput, +) -> Result { + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + let draft = session + .draft_json + .as_deref() + .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) + .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; + validate_asset_generate_input(&input, &draft).map_err(|error| error.to_string())?; + + let slot = build_generated_asset_slot( + &input.session_id, + &draft, + input.asset_kind, + input.level, + input.motion_key.clone(), + input.generated_at_micros, + ) + .map_err(|error| error.to_string())?; + upsert_big_fish_asset_slot(ctx, slot); + + let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id); + let coverage = build_asset_coverage(Some(&draft), &asset_slots); + let updated_at = Timestamp::from_micros_since_unix_epoch(input.generated_at_micros); + let reply = match input.asset_kind { + BigFishAssetKind::LevelMainImage => "本级主图已生成并设为正式资产。", + BigFishAssetKind::LevelMotion => "本级动作已生成并设为正式资产。", + BigFishAssetKind::StageBackground => "活动区域背景已生成并设为正式资产。", + } + .to_string(); + let next_stage = if coverage.publish_ready { + BigFishCreationStage::ReadyToPublish + } else { + BigFishCreationStage::AssetRefining + }; + let next_session = BigFishCreationSession { + session_id: session.session_id.clone(), + owner_user_id: session.owner_user_id.clone(), + seed_text: session.seed_text.clone(), + current_turn: session.current_turn, + progress_percent: if coverage.publish_ready { 96 } else { 88 }, + stage: next_stage, + anchor_pack_json: session.anchor_pack_json.clone(), + draft_json: session.draft_json.clone(), + asset_coverage_json: serialize_asset_coverage(&coverage) + .map_err(|error| error.to_string())?, + last_assistant_reply: Some(reply.clone()), + publish_ready: coverage.publish_ready, + created_at: session.created_at, + updated_at, + }; + replace_big_fish_session(ctx, &session, next_session); + append_big_fish_system_message( + ctx, + &input.session_id, + format!("big-fish-message-asset-{}", input.generated_at_micros), + reply, + input.generated_at_micros, + ); + + get_big_fish_session_tx( + ctx, + BigFishSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn publish_big_fish_game_tx( + ctx: &ReducerContext, + input: BigFishPublishInput, +) -> Result { + validate_publish_input(&input).map_err(|error| error.to_string())?; + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + let draft = session + .draft_json + .as_deref() + .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) + .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; + let coverage = build_asset_coverage(Some(&draft), &list_big_fish_asset_slots(ctx, &session.session_id)); + if !coverage.publish_ready { + return Err(format!("big_fish 发布校验未通过:{}", coverage.blockers.join(";"))); + } + + let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); + let next_session = BigFishCreationSession { + session_id: session.session_id.clone(), + owner_user_id: session.owner_user_id.clone(), + seed_text: session.seed_text.clone(), + current_turn: session.current_turn, + progress_percent: 100, + stage: BigFishCreationStage::Published, + anchor_pack_json: session.anchor_pack_json.clone(), + draft_json: session.draft_json.clone(), + asset_coverage_json: serialize_asset_coverage(&coverage) + .map_err(|error| error.to_string())?, + last_assistant_reply: Some("玩法已发布,可以进入测试运行态。".to_string()), + publish_ready: true, + created_at: session.created_at, + updated_at: published_at, + }; + replace_big_fish_session(ctx, &session, next_session); + + get_big_fish_session_tx( + ctx, + BigFishSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn start_big_fish_run_tx( + ctx: &ReducerContext, + input: BigFishRunStartInput, +) -> Result { + validate_run_start_input(&input).map_err(|error| error.to_string())?; + if ctx + .db + .big_fish_runtime_run() + .run_id() + .find(&input.run_id) + .is_some() + { + return Err("big_fish_runtime_run.run_id 已存在".to_string()); + } + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&input.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + let draft = session + .draft_json + .as_deref() + .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) + .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; + let snapshot = build_initial_runtime_snapshot( + input.run_id.clone(), + input.session_id.clone(), + &draft, + input.started_at_micros, + ); + let now = Timestamp::from_micros_since_unix_epoch(input.started_at_micros); + ctx.db.big_fish_runtime_run().insert(BigFishRuntimeRun { + run_id: input.run_id, + session_id: input.session_id, + owner_user_id: input.owner_user_id, + status: snapshot.status, + snapshot_json: serialize_runtime_snapshot(&snapshot).map_err(|error| error.to_string())?, + last_input_x: 0.0, + last_input_y: 0.0, + tick: snapshot.tick, + created_at: now, + updated_at: now, + }); + + Ok(snapshot) +} + +fn submit_big_fish_input_tx( + ctx: &ReducerContext, + input: BigFishRunInputSubmitInput, +) -> Result { + validate_run_input_submit_input(&input).map_err(|error| error.to_string())?; + let run = ctx + .db + .big_fish_runtime_run() + .run_id() + .find(&input.run_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_runtime_run 不存在".to_string())?; + let session = ctx + .db + .big_fish_creation_session() + .session_id() + .find(&run.session_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; + let draft = session + .draft_json + .as_deref() + .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) + .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; + let current_snapshot = + deserialize_runtime_snapshot(&run.snapshot_json).map_err(|error| error.to_string())?; + let next_snapshot = advance_runtime_snapshot( + current_snapshot, + &draft.runtime_params, + input.input_x, + input.input_y, + input.submitted_at_micros, + ); + replace_big_fish_run( + ctx, + &run, + BigFishRuntimeRun { + run_id: run.run_id.clone(), + session_id: run.session_id.clone(), + owner_user_id: run.owner_user_id.clone(), + status: next_snapshot.status, + snapshot_json: serialize_runtime_snapshot(&next_snapshot) + .map_err(|error| error.to_string())?, + last_input_x: input.input_x, + last_input_y: input.input_y, + tick: next_snapshot.tick, + created_at: run.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros), + }, + ); + + Ok(next_snapshot) +} + +fn get_big_fish_run_tx( + ctx: &ReducerContext, + input: BigFishRunGetInput, +) -> Result { + validate_run_get_input(&input).map_err(|error| error.to_string())?; + let run = ctx + .db + .big_fish_runtime_run() + .run_id() + .find(&input.run_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .ok_or_else(|| "big_fish_runtime_run 不存在".to_string())?; + + deserialize_runtime_snapshot(&run.snapshot_json).map_err(|error| error.to_string()) +} + fn create_custom_world_agent_session_tx( ctx: &ReducerContext, input: CustomWorldAgentSessionCreateInput, @@ -7908,3 +8626,155 @@ fn build_npc_state_snapshot_from_row(row: &NpcState) -> NpcStateSnapshot { updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), } } + +fn build_big_fish_session_snapshot( + ctx: &ReducerContext, + row: &BigFishCreationSession, +) -> Result { + let anchor_pack = + deserialize_anchor_pack(&row.anchor_pack_json).unwrap_or_else(|_| empty_anchor_pack()); + let draft = row + .draft_json + .as_deref() + .map(deserialize_draft) + .transpose() + .map_err(|error| format!("big_fish.draft_json 非法: {error}"))?; + let asset_slots = list_big_fish_asset_slots(ctx, &row.session_id); + let asset_coverage = build_asset_coverage(draft.as_ref(), &asset_slots); + let mut messages = ctx + .db + .big_fish_agent_message() + .iter() + .filter(|message| message.session_id == row.session_id) + .map(|message| BigFishAgentMessageSnapshot { + message_id: message.message_id, + session_id: message.session_id, + role: message.role, + kind: message.kind, + text: message.text, + created_at_micros: message.created_at.to_micros_since_unix_epoch(), + }) + .collect::>(); + messages.sort_by_key(|message| (message.created_at_micros, message.message_id.clone())); + + Ok(BigFishSessionSnapshot { + 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, + anchor_pack, + draft, + asset_slots, + asset_coverage, + messages, + last_assistant_reply: row.last_assistant_reply.clone(), + publish_ready: row.publish_ready, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + }) +} + +fn list_big_fish_asset_slots( + ctx: &ReducerContext, + session_id: &str, +) -> Vec { + let mut slots = ctx + .db + .big_fish_asset_slot() + .iter() + .filter(|slot| slot.session_id == session_id) + .map(|slot| BigFishAssetSlotSnapshot { + slot_id: slot.slot_id, + session_id: slot.session_id, + asset_kind: slot.asset_kind, + level: slot.level, + motion_key: slot.motion_key, + status: slot.status, + asset_url: slot.asset_url, + prompt_snapshot: slot.prompt_snapshot, + updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(), + }) + .collect::>(); + slots.sort_by_key(|slot| { + ( + slot.level.unwrap_or(0), + slot.asset_kind.as_str().to_string(), + slot.motion_key.clone().unwrap_or_default(), + slot.slot_id.clone(), + ) + }); + slots +} + +fn replace_big_fish_session( + ctx: &ReducerContext, + current: &BigFishCreationSession, + next: BigFishCreationSession, +) { + ctx.db + .big_fish_creation_session() + .session_id() + .delete(¤t.session_id); + ctx.db.big_fish_creation_session().insert(next); +} + +fn replace_big_fish_run(ctx: &ReducerContext, current: &BigFishRuntimeRun, next: BigFishRuntimeRun) { + ctx.db + .big_fish_runtime_run() + .run_id() + .delete(¤t.run_id); + ctx.db.big_fish_runtime_run().insert(next); +} + +fn upsert_big_fish_asset_slot(ctx: &ReducerContext, slot: BigFishAssetSlotSnapshot) { + if let Some(existing) = ctx + .db + .big_fish_asset_slot() + .slot_id() + .find(&slot.slot_id) + { + ctx.db + .big_fish_asset_slot() + .slot_id() + .delete(&existing.slot_id); + } + ctx.db.big_fish_asset_slot().insert(BigFishAssetSlot { + slot_id: slot.slot_id, + session_id: slot.session_id, + asset_kind: slot.asset_kind, + level: slot.level, + motion_key: slot.motion_key, + status: slot.status, + asset_url: slot.asset_url, + prompt_snapshot: slot.prompt_snapshot, + updated_at: Timestamp::from_micros_since_unix_epoch(slot.updated_at_micros), + }); +} + +fn append_big_fish_system_message( + ctx: &ReducerContext, + session_id: &str, + message_id: String, + text: String, + created_at_micros: i64, +) { + if ctx + .db + .big_fish_agent_message() + .message_id() + .find(&message_id) + .is_some() + { + return; + } + ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { + message_id, + session_id: session_id.to_string(), + role: BigFishAgentMessageRole::Assistant, + kind: BigFishAgentMessageKind::ActionResult, + text, + created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros), + }); +} diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs new file mode 100644 index 00000000..c39443cd --- /dev/null +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -0,0 +1,1419 @@ +use module_puzzle::{ + PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageKind, PuzzleAgentMessageRole, + PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput, PuzzleAgentSessionGetInput, + PuzzleAgentSessionProcedureResult, PuzzleAgentSessionSnapshot, PuzzleAgentStage, + PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate, + PuzzleGeneratedImagesSaveInput, PuzzlePublicationStatus, PuzzlePublishInput, + PuzzleResultDraft, PuzzleRunDragInput, PuzzleRunGetInput, PuzzleRunNextLevelInput, + PuzzleRunProcedureResult, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, + PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkGetInput, + PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkUpsertInput, + PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft, + apply_selected_candidate, + build_result_preview, compile_result_draft, create_work_profile, infer_anchor_pack, + normalize_theme_tags, publish_work_profile, resolve_puzzle_grid_size, + select_next_profile, start_run, swap_pieces, +}; +use serde_json::from_str as json_from_str; +use serde_json::to_string as json_to_string; +use spacetimedb::{ProcedureContext, Table, Timestamp, TxContext}; + +/// 拼图 Agent session 真相表。 +/// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。 +#[spacetimedb::table( + accessor = puzzle_agent_session, + index(accessor = by_puzzle_agent_session_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct PuzzleAgentSessionRow { + #[primary_key] + session_id: String, + owner_user_id: String, + seed_text: String, + current_turn: u32, + progress_percent: u32, + stage: PuzzleAgentStage, + anchor_pack_json: String, + draft_json: Option, + last_assistant_reply: Option, + published_profile_id: Option, + created_at: Timestamp, + updated_at: Timestamp, +} + +/// 拼图 Agent 消息真相表。 +#[spacetimedb::table( + accessor = puzzle_agent_message, + index(accessor = by_puzzle_agent_message_session_id, btree(columns = [session_id])) +)] +pub struct PuzzleAgentMessageRow { + #[primary_key] + message_id: String, + session_id: String, + role: PuzzleAgentMessageRole, + kind: PuzzleAgentMessageKind, + text: String, + created_at: Timestamp, +} + +/// 已发布与草稿作品统一作品表。 +#[spacetimedb::table( + accessor = puzzle_work_profile, + index(accessor = by_puzzle_work_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_puzzle_work_publication_status, btree(columns = [publication_status])) +)] +pub struct PuzzleWorkProfileRow { + #[primary_key] + profile_id: String, + work_id: String, + owner_user_id: String, + source_session_id: Option, + author_display_name: String, + level_name: String, + summary: String, + theme_tags_json: String, + cover_image_src: Option, + cover_asset_id: Option, + publication_status: PuzzlePublicationStatus, + play_count: u32, + anchor_pack_json: String, + publish_ready: bool, + created_at: Timestamp, + updated_at: Timestamp, + published_at: Option, +} + +/// 运行态 run 快照表。 +#[spacetimedb::table( + accessor = puzzle_runtime_run, + index(accessor = by_puzzle_runtime_run_owner_user_id, btree(columns = [owner_user_id])) +)] +pub struct PuzzleRuntimeRunRow { + #[primary_key] + run_id: String, + owner_user_id: String, + entry_profile_id: String, + current_profile_id: String, + cleared_level_count: u32, + current_level_index: u32, + current_grid_size: u32, + played_profile_ids_json: String, + previous_level_tags_json: String, + snapshot_json: String, + created_at: Timestamp, + updated_at: Timestamp, +} + +#[spacetimedb::procedure] +pub fn create_puzzle_agent_session( + ctx: &mut ProcedureContext, + input: PuzzleAgentSessionCreateInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| create_puzzle_agent_session_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_puzzle_agent_session( + ctx: &mut ProcedureContext, + input: PuzzleAgentSessionGetInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| get_puzzle_agent_session_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn submit_puzzle_agent_message( + ctx: &mut ProcedureContext, + input: module_puzzle::PuzzleAgentMessageSubmitInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| submit_puzzle_agent_message_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn compile_puzzle_agent_draft( + ctx: &mut ProcedureContext, + input: PuzzleDraftCompileInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| compile_puzzle_agent_draft_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn save_puzzle_generated_images( + ctx: &mut ProcedureContext, + input: PuzzleGeneratedImagesSaveInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| save_puzzle_generated_images_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn select_puzzle_cover_image( + ctx: &mut ProcedureContext, + input: PuzzleSelectCoverImageInput, +) -> PuzzleAgentSessionProcedureResult { + match ctx.try_with_tx(|tx| select_puzzle_cover_image_tx(tx, input.clone())) { + Ok(session) => PuzzleAgentSessionProcedureResult { + ok: true, + session_json: Some(serialize_json(&session)), + error_message: None, + }, + Err(message) => PuzzleAgentSessionProcedureResult { + ok: false, + session_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn publish_puzzle_work( + ctx: &mut ProcedureContext, + input: PuzzlePublishInput, +) -> PuzzleWorkProcedureResult { + match ctx.try_with_tx(|tx| publish_puzzle_work_tx(tx, input.clone())) { + Ok(item) => PuzzleWorkProcedureResult { + ok: true, + item_json: Some(serialize_json(&item)), + error_message: None, + }, + Err(message) => PuzzleWorkProcedureResult { + ok: false, + item_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn list_puzzle_works( + ctx: &mut ProcedureContext, + input: PuzzleWorksListInput, +) -> PuzzleWorksProcedureResult { + match ctx.try_with_tx(|tx| list_puzzle_works_tx(tx, input.clone())) { + Ok(items) => PuzzleWorksProcedureResult { + ok: true, + items_json: Some(serialize_json(&items)), + error_message: None, + }, + Err(message) => PuzzleWorksProcedureResult { + ok: false, + items_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_puzzle_work_detail( + ctx: &mut ProcedureContext, + input: PuzzleWorkGetInput, +) -> PuzzleWorkProcedureResult { + match ctx.try_with_tx(|tx| get_puzzle_work_detail_tx(tx, input.clone())) { + Ok(item) => PuzzleWorkProcedureResult { + ok: true, + item_json: Some(serialize_json(&item)), + error_message: None, + }, + Err(message) => PuzzleWorkProcedureResult { + ok: false, + item_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn update_puzzle_work( + ctx: &mut ProcedureContext, + input: PuzzleWorkUpsertInput, +) -> PuzzleWorkProcedureResult { + match ctx.try_with_tx(|tx| update_puzzle_work_tx(tx, input.clone())) { + Ok(item) => PuzzleWorkProcedureResult { + ok: true, + item_json: Some(serialize_json(&item)), + error_message: None, + }, + Err(message) => PuzzleWorkProcedureResult { + ok: false, + item_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn list_puzzle_gallery(ctx: &mut ProcedureContext) -> PuzzleWorksProcedureResult { + match ctx.try_with_tx(|tx| list_puzzle_gallery_tx(tx)) { + Ok(items) => PuzzleWorksProcedureResult { + ok: true, + items_json: Some(serialize_json(&items)), + error_message: None, + }, + Err(message) => PuzzleWorksProcedureResult { + ok: false, + items_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_puzzle_gallery_detail( + ctx: &mut ProcedureContext, + input: PuzzleWorkGetInput, +) -> PuzzleWorkProcedureResult { + match ctx.try_with_tx(|tx| get_puzzle_gallery_detail_tx(tx, input.clone())) { + Ok(item) => PuzzleWorkProcedureResult { + ok: true, + item_json: Some(serialize_json(&item)), + error_message: None, + }, + Err(message) => PuzzleWorkProcedureResult { + ok: false, + item_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn start_puzzle_run( + ctx: &mut ProcedureContext, + input: PuzzleRunStartInput, +) -> PuzzleRunProcedureResult { + match ctx.try_with_tx(|tx| start_puzzle_run_tx(tx, input.clone())) { + Ok(run) => PuzzleRunProcedureResult { + ok: true, + run_json: Some(serialize_json(&run)), + error_message: None, + }, + Err(message) => PuzzleRunProcedureResult { + ok: false, + run_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn get_puzzle_run( + ctx: &mut ProcedureContext, + input: PuzzleRunGetInput, +) -> PuzzleRunProcedureResult { + match ctx.try_with_tx(|tx| get_puzzle_run_tx(tx, input.clone())) { + Ok(run) => PuzzleRunProcedureResult { + ok: true, + run_json: Some(serialize_json(&run)), + error_message: None, + }, + Err(message) => PuzzleRunProcedureResult { + ok: false, + run_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn swap_puzzle_pieces( + ctx: &mut ProcedureContext, + input: PuzzleRunSwapInput, +) -> PuzzleRunProcedureResult { + match ctx.try_with_tx(|tx| swap_puzzle_pieces_tx(tx, input.clone())) { + Ok(run) => PuzzleRunProcedureResult { + ok: true, + run_json: Some(serialize_json(&run)), + error_message: None, + }, + Err(message) => PuzzleRunProcedureResult { + ok: false, + run_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn drag_puzzle_piece_or_group( + ctx: &mut ProcedureContext, + input: PuzzleRunDragInput, +) -> PuzzleRunProcedureResult { + match ctx.try_with_tx(|tx| drag_puzzle_piece_or_group_tx(tx, input.clone())) { + Ok(run) => PuzzleRunProcedureResult { + ok: true, + run_json: Some(serialize_json(&run)), + error_message: None, + }, + Err(message) => PuzzleRunProcedureResult { + ok: false, + run_json: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn advance_puzzle_next_level( + ctx: &mut ProcedureContext, + input: PuzzleRunNextLevelInput, +) -> PuzzleRunProcedureResult { + match ctx.try_with_tx(|tx| advance_puzzle_next_level_tx(tx, input.clone())) { + Ok(run) => PuzzleRunProcedureResult { + ok: true, + run_json: Some(serialize_json(&run)), + error_message: None, + }, + Err(message) => PuzzleRunProcedureResult { + ok: false, + run_json: None, + error_message: Some(message), + }, + } +} + +fn create_puzzle_agent_session_tx( + ctx: &TxContext, + input: PuzzleAgentSessionCreateInput, +) -> Result { + ensure_session_missing(ctx, &input.session_id)?; + ensure_message_missing(ctx, &input.welcome_message_id)?; + let created_at = Timestamp::from_micros_since_unix_epoch(input.created_at_micros); + let anchor_pack = infer_anchor_pack(&input.seed_text, Some(&input.seed_text)); + ctx.db.puzzle_agent_session().insert(PuzzleAgentSessionRow { + session_id: input.session_id.clone(), + owner_user_id: input.owner_user_id.clone(), + seed_text: input.seed_text.clone(), + current_turn: 1, + progress_percent: 18, + stage: PuzzleAgentStage::CollectingAnchors, + anchor_pack_json: serialize_json(&anchor_pack), + draft_json: None, + last_assistant_reply: Some(input.welcome_message_text.clone()), + published_profile_id: None, + created_at, + updated_at: created_at, + }); + ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow { + message_id: input.welcome_message_id, + session_id: input.session_id.clone(), + role: PuzzleAgentMessageRole::Assistant, + kind: PuzzleAgentMessageKind::Chat, + text: input.welcome_message_text, + created_at, + }); + get_puzzle_agent_session_tx( + ctx, + PuzzleAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn get_puzzle_agent_session_tx( + ctx: &TxContext, + input: PuzzleAgentSessionGetInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + build_puzzle_agent_session_snapshot(ctx, &row) +} + +fn submit_puzzle_agent_message_tx( + ctx: &TxContext, + input: module_puzzle::PuzzleAgentMessageSubmitInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + ensure_message_missing(ctx, &input.user_message_id)?; + let submitted_at = Timestamp::from_micros_since_unix_epoch(input.submitted_at_micros); + let next_anchor_pack = infer_anchor_pack(&row.seed_text, Some(&input.user_message_text)); + let assistant_message_text = build_puzzle_assistant_reply(&next_anchor_pack); + + ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow { + message_id: input.user_message_id.clone(), + session_id: input.session_id.clone(), + role: PuzzleAgentMessageRole::User, + kind: PuzzleAgentMessageKind::Chat, + text: input.user_message_text.clone(), + created_at: submitted_at, + }); + let assistant_message_id = format!("{}assistant-{}", input.session_id, input.submitted_at_micros); + ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow { + message_id: assistant_message_id, + session_id: input.session_id.clone(), + role: PuzzleAgentMessageRole::Assistant, + kind: PuzzleAgentMessageKind::Summary, + text: assistant_message_text.clone(), + created_at: submitted_at, + }); + + replace_puzzle_agent_session( + ctx, + &row, + PuzzleAgentSessionRow { + 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.saturating_add(1), + progress_percent: (row.progress_percent + 18).min(82), + stage: PuzzleAgentStage::CollectingAnchors, + anchor_pack_json: serialize_json(&next_anchor_pack), + draft_json: row.draft_json.clone(), + last_assistant_reply: Some(assistant_message_text), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: submitted_at, + }, + ); + + get_puzzle_agent_session_tx( + ctx, + PuzzleAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn compile_puzzle_agent_draft_tx( + ctx: &TxContext, + input: PuzzleDraftCompileInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + let anchor_pack = deserialize_anchor_pack(&row.anchor_pack_json)?; + let messages = list_session_messages(ctx, &row.session_id); + let draft = compile_result_draft(&anchor_pack, &messages); + let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + replace_puzzle_agent_session( + ctx, + &row, + PuzzleAgentSessionRow { + 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: 88, + stage: PuzzleAgentStage::DraftReady, + anchor_pack_json: serialize_json(&anchor_pack), + draft_json: Some(serialize_json(&draft)), + last_assistant_reply: Some("拼图结果页草稿已经生成,可以开始出图并确认标签。".to_string()), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: compiled_at, + }, + ); + append_system_message( + ctx, + &row.session_id, + input.compiled_at_micros, + "拼图结果页草稿已生成。", + )?; + get_puzzle_agent_session_tx( + ctx, + PuzzleAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn save_puzzle_generated_images_tx( + ctx: &TxContext, + input: PuzzleGeneratedImagesSaveInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + let mut draft = deserialize_draft_required(&row.draft_json)?; + let candidates: Vec = + json_from_str(&input.candidates_json).map_err(|error| format!("拼图候选图 JSON 非法: {error}"))?; + if candidates.is_empty() { + return Err("拼图候选图不能为空".to_string()); + } + draft.candidates = candidates; + draft.generation_status = "ready".to_string(); + if let Some(selected) = draft.candidates.iter().find(|entry| entry.selected).cloned() { + draft.selected_candidate_id = Some(selected.candidate_id); + draft.cover_image_src = Some(selected.image_src); + draft.cover_asset_id = Some(selected.asset_id); + } + + let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros); + let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready { + PuzzleAgentStage::ReadyToPublish + } else { + PuzzleAgentStage::ImageRefining + }; + replace_puzzle_agent_session( + ctx, + &row, + PuzzleAgentSessionRow { + 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: 94, + stage: next_stage, + anchor_pack_json: row.anchor_pack_json.clone(), + draft_json: Some(serialize_json(&draft)), + last_assistant_reply: Some("候选图已经生成,请选择正式拼图图片。".to_string()), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: saved_at, + }, + ); + get_puzzle_agent_session_tx( + ctx, + PuzzleAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn select_puzzle_cover_image_tx( + ctx: &TxContext, + input: PuzzleSelectCoverImageInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + let draft = deserialize_draft_required(&row.draft_json)?; + let draft = apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?; + let selected_at = Timestamp::from_micros_since_unix_epoch(input.selected_at_micros); + let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready { + PuzzleAgentStage::ReadyToPublish + } else { + PuzzleAgentStage::ImageRefining + }; + replace_puzzle_agent_session( + ctx, + &row, + PuzzleAgentSessionRow { + 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: 96, + stage: next_stage, + anchor_pack_json: row.anchor_pack_json.clone(), + draft_json: Some(serialize_json(&draft)), + last_assistant_reply: Some("正式拼图图片已确定,可以准备发布。".to_string()), + published_profile_id: row.published_profile_id.clone(), + created_at: row.created_at, + updated_at: selected_at, + }, + ); + get_puzzle_agent_session_tx( + ctx, + PuzzleAgentSessionGetInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + }, + ) +} + +fn publish_puzzle_work_tx( + ctx: &TxContext, + input: PuzzlePublishInput, +) -> Result { + let row = get_owned_session_row(ctx, &input.session_id, &input.owner_user_id)?; + let draft = deserialize_draft_required(&row.draft_json)?; + let draft = apply_publish_overrides_to_draft( + &draft, + input.level_name.clone(), + input.summary.clone(), + input.theme_tags.clone(), + ) + .map_err(|error| error.to_string())?; + let mut profile = create_work_profile( + input.work_id.clone(), + input.profile_id.clone(), + input.owner_user_id.clone(), + Some(input.session_id.clone()), + input.author_display_name.clone(), + &draft, + input.published_at_micros, + ) + .map_err(|error| error.to_string())?; + profile = publish_work_profile(profile, &draft, input.published_at_micros) + .map_err(|error| error.to_string())?; + + upsert_puzzle_work_profile(ctx, profile.clone())?; + replace_puzzle_agent_session( + ctx, + &row, + PuzzleAgentSessionRow { + 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: 100, + stage: PuzzleAgentStage::Published, + anchor_pack_json: row.anchor_pack_json.clone(), + draft_json: Some(serialize_json(&draft)), + last_assistant_reply: Some("拼图作品已经发布到广场。".to_string()), + published_profile_id: Some(profile.profile_id.clone()), + created_at: row.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(input.published_at_micros), + }, + ); + + Ok(profile) +} + +fn list_puzzle_works_tx( + ctx: &TxContext, + input: PuzzleWorksListInput, +) -> Result, String> { + let mut items = ctx + .db + .puzzle_work_profile() + .iter() + .filter(|row| row.owner_user_id == input.owner_user_id) + .map(|row| build_puzzle_work_profile_from_row(&row)) + .collect::, _>>()?; + items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros)); + Ok(items) +} + +fn get_puzzle_work_detail_tx( + ctx: &TxContext, + input: PuzzleWorkGetInput, +) -> Result { + let row = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&input.profile_id) + .ok_or_else(|| "拼图作品不存在".to_string())?; + build_puzzle_work_profile_from_row(&row) +} + +fn update_puzzle_work_tx( + ctx: &TxContext, + input: PuzzleWorkUpsertInput, +) -> Result { + let row = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&input.profile_id) + .ok_or_else(|| "拼图作品不存在".to_string())?; + if row.owner_user_id != input.owner_user_id { + return Err("无权修改该拼图作品".to_string()); + } + let theme_tags = normalize_theme_tags(input.theme_tags); + if theme_tags.is_empty() || theme_tags.len() > PUZZLE_MAX_TAG_COUNT { + return Err("拼图标签数量不合法".to_string()); + } + let next_row = PuzzleWorkProfileRow { + 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(), + level_name: input.level_name, + summary: input.summary, + theme_tags_json: serialize_json(&theme_tags), + cover_image_src: input.cover_image_src, + cover_asset_id: input.cover_asset_id, + publication_status: row.publication_status, + play_count: row.play_count, + anchor_pack_json: row.anchor_pack_json.clone(), + publish_ready: row.publish_ready, + created_at: row.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros), + published_at: row.published_at, + }; + replace_puzzle_work_profile(ctx, &row, next_row); + get_puzzle_work_detail_tx( + ctx, + PuzzleWorkGetInput { + profile_id: input.profile_id, + }, + ) +} + +fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result, String> { + let mut items = ctx + .db + .puzzle_work_profile() + .iter() + .filter(|row| row.publication_status == PuzzlePublicationStatus::Published) + .map(|row| build_puzzle_work_profile_from_row(&row)) + .collect::, _>>()?; + items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros)); + Ok(items) +} + +fn get_puzzle_gallery_detail_tx( + ctx: &TxContext, + input: PuzzleWorkGetInput, +) -> Result { + let row = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&input.profile_id) + .ok_or_else(|| "拼图作品不存在".to_string())?; + if row.publication_status != PuzzlePublicationStatus::Published { + return Err("拼图作品尚未发布".to_string()); + } + build_puzzle_work_profile_from_row(&row) +} + +fn start_puzzle_run_tx( + ctx: &TxContext, + input: PuzzleRunStartInput, +) -> Result { + if ctx.db.puzzle_runtime_run().run_id().find(&input.run_id).is_some() { + return Err("拼图 run 已存在".to_string()); + } + let entry_profile_row = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&input.profile_id) + .ok_or_else(|| "入口拼图作品不存在".to_string())?; + if entry_profile_row.publication_status != PuzzlePublicationStatus::Published { + return Err("入口拼图作品未发布".to_string()); + } + let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?; + let mut run = start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?; + run.recommended_next_profile_id = select_next_profile( + &entry_profile, + &run.played_profile_ids, + &list_published_puzzle_profiles(ctx)?, + ) + .map(|value| value.profile_id.clone()); + + increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros); + insert_puzzle_runtime_run(ctx, &run, &input.owner_user_id, input.started_at_micros)?; + Ok(run) +} + +fn get_puzzle_run_tx( + ctx: &TxContext, + input: PuzzleRunGetInput, +) -> Result { + let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?; + deserialize_run(&row.snapshot_json) +} + +fn swap_puzzle_pieces_tx( + ctx: &TxContext, + input: PuzzleRunSwapInput, +) -> Result { + let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?; + let current_run = deserialize_run(&row.snapshot_json)?; + let mut next_run = + swap_pieces(¤t_run, &input.first_piece_id, &input.second_piece_id).map_err(|error| error.to_string())?; + refresh_next_profile_recommendation(ctx, &mut next_run)?; + replace_puzzle_runtime_run(ctx, &row, &next_run, input.swapped_at_micros); + Ok(next_run) +} + +fn drag_puzzle_piece_or_group_tx( + ctx: &TxContext, + input: PuzzleRunDragInput, +) -> Result { + let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?; + let current_run = deserialize_run(&row.snapshot_json)?; + let mut next_run = module_puzzle::drag_piece_or_group( + ¤t_run, + &input.piece_id, + input.target_row, + input.target_col, + ) + .map_err(|error| error.to_string())?; + refresh_next_profile_recommendation(ctx, &mut next_run)?; + replace_puzzle_runtime_run(ctx, &row, &next_run, input.dragged_at_micros); + Ok(next_run) +} + +fn advance_puzzle_next_level_tx( + ctx: &TxContext, + input: PuzzleRunNextLevelInput, +) -> Result { + let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?; + let current_run = deserialize_run(&row.snapshot_json)?; + let current_level = current_run + .current_level + .as_ref() + .ok_or_else(|| "拼图关卡不存在".to_string())?; + if current_level.status != PuzzleRuntimeLevelStatus::Cleared { + return Err("当前关卡尚未通关".to_string()); + } + let current_profile = build_puzzle_work_profile_from_row( + &ctx.db + .puzzle_work_profile() + .profile_id() + .find(¤t_level.profile_id) + .ok_or_else(|| "当前拼图作品不存在".to_string())?, + )?; + let candidates = list_published_puzzle_profiles(ctx)?; + let next_profile = select_next_profile(¤t_profile, ¤t_run.played_profile_ids, &candidates) + .ok_or_else(|| "没有可用的下一关候选".to_string())? + .clone(); + let mut next_run = + module_puzzle::advance_next_level(¤t_run, &next_profile).map_err(|error| error.to_string())?; + next_run.recommended_next_profile_id = select_next_profile( + &next_profile, + &next_run.played_profile_ids, + &candidates, + ) + .map(|value| value.profile_id.clone()); + + if let Some(next_profile_row) = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&next_profile.profile_id) + { + increment_puzzle_profile_play_count(ctx, &next_profile_row, input.advanced_at_micros); + } + replace_puzzle_runtime_run(ctx, &row, &next_run, input.advanced_at_micros); + Ok(next_run) +} + +fn build_puzzle_agent_session_snapshot( + ctx: &TxContext, + row: &PuzzleAgentSessionRow, +) -> Result { + let anchor_pack = deserialize_anchor_pack(&row.anchor_pack_json)?; + let draft = deserialize_optional_draft(&row.draft_json)?; + let messages = list_session_messages(ctx, &row.session_id); + let result_preview = draft + .as_ref() + .map(|value| build_result_preview(value, Some("创作者"))); + + Ok(PuzzleAgentSessionSnapshot { + 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, + anchor_pack, + draft, + messages, + last_assistant_reply: row.last_assistant_reply.clone(), + published_profile_id: row.published_profile_id.clone(), + suggested_actions: build_puzzle_suggested_actions(row.stage), + result_preview, + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + }) +} + +fn build_puzzle_work_profile_from_row(row: &PuzzleWorkProfileRow) -> Result { + Ok(PuzzleWorkProfile { + 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(), + level_name: row.level_name.clone(), + summary: row.summary.clone(), + theme_tags: deserialize_theme_tags(&row.theme_tags_json)?, + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + publication_status: row.publication_status, + 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()), + play_count: row.play_count, + publish_ready: row.publish_ready, + anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?, + }) +} + +fn list_session_messages(ctx: &TxContext, session_id: &str) -> Vec { + let mut items = ctx + .db + .puzzle_agent_message() + .iter() + .filter(|message| message.session_id == session_id) + .map(|message| PuzzleAgentMessageSnapshot { + message_id: message.message_id.clone(), + session_id: message.session_id.clone(), + role: message.role, + kind: message.kind, + text: message.text.clone(), + created_at_micros: message.created_at.to_micros_since_unix_epoch(), + }) + .collect::>(); + items.sort_by(|left, right| left.created_at_micros.cmp(&right.created_at_micros)); + items +} + +fn build_puzzle_suggested_actions(stage: PuzzleAgentStage) -> Vec { + match stage { + PuzzleAgentStage::CollectingAnchors => vec![module_puzzle::PuzzleAgentSuggestedAction { + id: "compile-draft".to_string(), + action_type: "compile_puzzle_draft".to_string(), + label: "进入结果页".to_string(), + }], + PuzzleAgentStage::DraftReady | PuzzleAgentStage::ImageRefining => vec![ + module_puzzle::PuzzleAgentSuggestedAction { + id: "generate-images".to_string(), + action_type: "generate_puzzle_images".to_string(), + label: "生成候选图".to_string(), + }, + module_puzzle::PuzzleAgentSuggestedAction { + id: "publish-work".to_string(), + action_type: "publish_puzzle_work".to_string(), + label: "发布作品".to_string(), + }, + ], + PuzzleAgentStage::ReadyToPublish => vec![module_puzzle::PuzzleAgentSuggestedAction { + id: "publish-work".to_string(), + action_type: "publish_puzzle_work".to_string(), + label: "发布作品".to_string(), + }], + PuzzleAgentStage::Published => Vec::new(), + } +} + +fn build_puzzle_assistant_reply(anchor_pack: &PuzzleAnchorPack) -> String { + format!( + "我先帮你收束成一版拼图方向:题材是“{}”,主体聚焦“{}”,氛围偏“{}”。", + anchor_pack.theme_promise.value, + anchor_pack.visual_subject.value, + anchor_pack.visual_mood.value + ) +} + +fn append_system_message( + ctx: &TxContext, + session_id: &str, + created_at_micros: i64, + text: &str, +) -> Result<(), String> { + let message_id = format!("{session_id}-system-{created_at_micros}"); + ensure_message_missing(ctx, &message_id)?; + ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow { + message_id, + session_id: session_id.to_string(), + role: PuzzleAgentMessageRole::Assistant, + kind: PuzzleAgentMessageKind::ActionResult, + text: text.to_string(), + created_at: Timestamp::from_micros_since_unix_epoch(created_at_micros), + }); + Ok(()) +} + +fn ensure_session_missing(ctx: &TxContext, session_id: &str) -> Result<(), String> { + if ctx.db.puzzle_agent_session().session_id().find(&session_id.to_string()).is_some() { + return Err("拼图 session 已存在".to_string()); + } + Ok(()) +} + +fn ensure_message_missing(ctx: &TxContext, message_id: &str) -> Result<(), String> { + if ctx.db.puzzle_agent_message().message_id().find(&message_id.to_string()).is_some() { + return Err("拼图消息已存在".to_string()); + } + Ok(()) +} + +fn get_owned_session_row( + ctx: &TxContext, + session_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .puzzle_agent_session() + .session_id() + .find(&session_id.to_string()) + .ok_or_else(|| "拼图 session 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该拼图 session".to_string()); + } + Ok(row) +} + +fn get_owned_run_row( + ctx: &TxContext, + run_id: &str, + owner_user_id: &str, +) -> Result { + let row = ctx + .db + .puzzle_runtime_run() + .run_id() + .find(&run_id.to_string()) + .ok_or_else(|| "拼图 run 不存在".to_string())?; + if row.owner_user_id != owner_user_id { + return Err("无权访问该拼图 run".to_string()); + } + Ok(row) +} + +fn replace_puzzle_agent_session( + ctx: &TxContext, + current: &PuzzleAgentSessionRow, + next: PuzzleAgentSessionRow, +) { + ctx.db + .puzzle_agent_session() + .session_id() + .delete(¤t.session_id); + ctx.db.puzzle_agent_session().insert(next); +} + +fn replace_puzzle_work_profile( + ctx: &TxContext, + current: &PuzzleWorkProfileRow, + next: PuzzleWorkProfileRow, +) { + ctx.db + .puzzle_work_profile() + .profile_id() + .delete(¤t.profile_id); + ctx.db.puzzle_work_profile().insert(next); +} + +fn upsert_puzzle_work_profile( + ctx: &TxContext, + profile: PuzzleWorkProfile, +) -> Result<(), String> { + if let Some(existing) = ctx + .db + .puzzle_work_profile() + .profile_id() + .find(&profile.profile_id) + { + replace_puzzle_work_profile( + ctx, + &existing, + PuzzleWorkProfileRow { + profile_id: profile.profile_id, + work_id: profile.work_id, + owner_user_id: profile.owner_user_id, + source_session_id: profile.source_session_id, + author_display_name: profile.author_display_name, + level_name: profile.level_name, + summary: profile.summary, + theme_tags_json: serialize_json(&profile.theme_tags), + cover_image_src: profile.cover_image_src, + cover_asset_id: profile.cover_asset_id, + publication_status: profile.publication_status, + play_count: profile.play_count, + anchor_pack_json: serialize_json(&profile.anchor_pack), + publish_ready: profile.publish_ready, + created_at: existing.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros), + published_at: profile + .published_at_micros + .map(Timestamp::from_micros_since_unix_epoch), + }, + ); + return Ok(()); + } + + ctx.db.puzzle_work_profile().insert(PuzzleWorkProfileRow { + profile_id: profile.profile_id, + work_id: profile.work_id, + owner_user_id: profile.owner_user_id, + source_session_id: profile.source_session_id, + author_display_name: profile.author_display_name, + level_name: profile.level_name, + summary: profile.summary, + theme_tags_json: serialize_json(&profile.theme_tags), + cover_image_src: profile.cover_image_src, + cover_asset_id: profile.cover_asset_id, + publication_status: profile.publication_status, + play_count: profile.play_count, + anchor_pack_json: serialize_json(&profile.anchor_pack), + publish_ready: profile.publish_ready, + created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros), + updated_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros), + published_at: profile + .published_at_micros + .map(Timestamp::from_micros_since_unix_epoch), + }); + Ok(()) +} + +fn insert_puzzle_runtime_run( + ctx: &TxContext, + run: &PuzzleRunSnapshot, + owner_user_id: &str, + created_at_micros: i64, +) -> Result<(), String> { + let timestamp = Timestamp::from_micros_since_unix_epoch(created_at_micros); + let current_profile_id = run + .current_level + .as_ref() + .map(|level| level.profile_id.clone()) + .ok_or_else(|| "拼图关卡不存在".to_string())?; + ctx.db.puzzle_runtime_run().insert(PuzzleRuntimeRunRow { + run_id: run.run_id.clone(), + owner_user_id: owner_user_id.to_string(), + entry_profile_id: run.entry_profile_id.clone(), + current_profile_id, + cleared_level_count: run.cleared_level_count, + current_level_index: run.current_level_index, + current_grid_size: run.current_grid_size, + played_profile_ids_json: serialize_json(&run.played_profile_ids), + previous_level_tags_json: serialize_json(&run.previous_level_tags), + snapshot_json: serialize_json(run), + created_at: timestamp, + updated_at: timestamp, + }); + Ok(()) +} + +fn replace_puzzle_runtime_run( + ctx: &TxContext, + current: &PuzzleRuntimeRunRow, + run: &PuzzleRunSnapshot, + updated_at_micros: i64, +) { + ctx.db + .puzzle_runtime_run() + .run_id() + .delete(¤t.run_id); + ctx.db.puzzle_runtime_run().insert(PuzzleRuntimeRunRow { + run_id: run.run_id.clone(), + owner_user_id: current.owner_user_id.clone(), + entry_profile_id: run.entry_profile_id.clone(), + current_profile_id: run + .current_level + .as_ref() + .map(|level| level.profile_id.clone()) + .unwrap_or_else(|| current.current_profile_id.clone()), + cleared_level_count: run.cleared_level_count, + current_level_index: run.current_level_index, + current_grid_size: resolve_puzzle_grid_size(run.cleared_level_count), + played_profile_ids_json: serialize_json(&run.played_profile_ids), + previous_level_tags_json: serialize_json(&run.previous_level_tags), + snapshot_json: serialize_json(run), + created_at: current.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), + }); +} + +fn increment_puzzle_profile_play_count( + ctx: &TxContext, + row: &PuzzleWorkProfileRow, + updated_at_micros: i64, +) { + replace_puzzle_work_profile( + ctx, + row, + PuzzleWorkProfileRow { + 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(), + level_name: row.level_name.clone(), + summary: row.summary.clone(), + theme_tags_json: row.theme_tags_json.clone(), + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + publication_status: row.publication_status, + play_count: row.play_count.saturating_add(1), + anchor_pack_json: row.anchor_pack_json.clone(), + publish_ready: row.publish_ready, + created_at: row.created_at, + updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), + published_at: row.published_at, + }, + ); +} + +fn list_published_puzzle_profiles(ctx: &TxContext) -> Result, String> { + ctx.db + .puzzle_work_profile() + .iter() + .filter(|row| row.publication_status == PuzzlePublicationStatus::Published) + .map(|row| build_puzzle_work_profile_from_row(&row)) + .collect() +} + +fn refresh_next_profile_recommendation( + ctx: &TxContext, + run: &mut PuzzleRunSnapshot, +) -> Result<(), String> { + let current_level = match run.current_level.as_ref() { + Some(value) => value, + None => { + run.recommended_next_profile_id = None; + return Ok(()); + } + }; + let current_profile = build_puzzle_work_profile_from_row( + &ctx.db + .puzzle_work_profile() + .profile_id() + .find(¤t_level.profile_id) + .ok_or_else(|| "当前拼图作品不存在".to_string())?, + )?; + run.recommended_next_profile_id = select_next_profile( + ¤t_profile, + &run.played_profile_ids, + &list_published_puzzle_profiles(ctx)?, + ) + .map(|value| value.profile_id.clone()); + Ok(()) +} + +fn serialize_json(value: &T) -> String { + json_to_string(value).unwrap_or_else(|_| "{}".to_string()) +} + +fn deserialize_anchor_pack(value: &str) -> Result { + json_from_str(value).map_err(|error| format!("拼图 anchor_pack JSON 非法: {error}")) +} + +fn deserialize_optional_draft(value: &Option) -> Result, String> { + value + .as_ref() + .map(|raw| json_from_str(raw).map_err(|error| format!("拼图 draft JSON 非法: {error}"))) + .transpose() +} + +fn deserialize_draft_required(value: &Option) -> Result { + deserialize_optional_draft(value)?.ok_or_else(|| "拼图 draft 尚未生成".to_string()) +} + +fn deserialize_theme_tags(value: &str) -> Result, String> { + json_from_str(value).map_err(|error| format!("拼图 theme_tags JSON 非法: {error}")) +} + +fn deserialize_run(value: &str) -> Result { + json_from_str(value).map_err(|error| format!("拼图 run snapshot JSON 非法: {error}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn puzzle_json_round_trip_keeps_snapshot_shape() { + let snapshot = PuzzleRunSnapshot { + run_id: "run-1".to_string(), + entry_profile_id: "profile-1".to_string(), + cleared_level_count: 0, + current_level_index: 1, + current_grid_size: 3, + played_profile_ids: vec!["profile-1".to_string()], + previous_level_tags: vec!["蒸汽城市".to_string()], + current_level: None, + recommended_next_profile_id: None, + }; + let serialized = serialize_json(&snapshot); + let parsed = deserialize_run(&serialized).expect("run json should parse"); + assert_eq!(parsed.run_id, "run-1"); + } + + #[test] + fn puzzle_preview_is_publishable_with_complete_draft() { + let anchor_pack = infer_anchor_pack("蒸汽城市雨夜猫咪", Some("蒸汽城市雨夜猫咪")); + let draft = compile_result_draft(&anchor_pack, &[]); + let candidates = build_generated_candidates("session-1", None, &draft, 2, 1_000_000) + .expect("candidates should build"); + let draft = apply_selected_candidate( + PuzzleResultDraft { + candidates, + ..draft + }, + "session-1-candidate-1", + ) + .expect("draft should select"); + let preview = build_result_preview(&draft, Some("作者")); + assert!(preview.publish_ready); + } + + #[test] + fn puzzle_recommendation_score_prefers_same_author_weight() { + let left = PuzzleWorkProfile { + work_id: "work-a".to_string(), + profile_id: "profile-a".to_string(), + owner_user_id: "owner-a".to_string(), + source_session_id: None, + author_display_name: "作者".to_string(), + level_name: "A".to_string(), + summary: String::new(), + theme_tags: vec!["雨夜".to_string(), "猫咪".to_string()], + cover_image_src: Some("/a.png".to_string()), + cover_asset_id: Some("asset-a".to_string()), + publication_status: PuzzlePublicationStatus::Published, + updated_at_micros: 1, + published_at_micros: Some(1), + play_count: 0, + publish_ready: true, + anchor_pack: empty_anchor_pack(), + }; + let right = PuzzleWorkProfile { + owner_user_id: "owner-a".to_string(), + profile_id: "profile-b".to_string(), + work_id: "work-b".to_string(), + level_name: "B".to_string(), + theme_tags: vec!["雨夜".to_string(), "蒸汽城市".to_string()], + cover_image_src: Some("/b.png".to_string()), + cover_asset_id: Some("asset-b".to_string()), + publication_status: PuzzlePublicationStatus::Published, + updated_at_micros: 2, + published_at_micros: Some(2), + play_count: 0, + publish_ready: true, + anchor_pack: empty_anchor_pack(), + source_session_id: None, + author_display_name: "作者".to_string(), + summary: String::new(), + }; + assert!(recommendation_score(&left, &right) > tag_similarity_score(&left.theme_tags, &right.theme_tags)); + } +} diff --git a/server-rs/scripts/dev.ps1 b/server-rs/scripts/dev.ps1 index a8c7e5ae..f504fb4d 100644 --- a/server-rs/scripts/dev.ps1 +++ b/server-rs/scripts/dev.ps1 @@ -3,7 +3,7 @@ param( [Alias("h")] [switch]$Help, [string]$ApiHost = "127.0.0.1", - [int]$Port = 3000, + [int]$Port = 3100, [string]$Log = "info,tower_http=info" ) diff --git a/server-rs/scripts/dev.sh b/server-rs/scripts/dev.sh index d9a66f2a..23d27948 100644 --- a/server-rs/scripts/dev.sh +++ b/server-rs/scripts/dev.sh @@ -29,7 +29,7 @@ if [[ ! -f "${SERVER_RS_DIR}/Cargo.toml" ]]; then fi export GENARRATIVE_API_HOST="${GENARRATIVE_API_HOST:-127.0.0.1}" -export GENARRATIVE_API_PORT="${GENARRATIVE_API_PORT:-3000}" +export GENARRATIVE_API_PORT="${GENARRATIVE_API_PORT:-3100}" export GENARRATIVE_API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}" echo "[server-rs:dev] 工作目录: ${SERVER_RS_DIR}" diff --git a/src/components/auth/AccountModal.test.tsx b/src/components/auth/AccountModal.test.tsx index f88a269d..1ea09812 100644 --- a/src/components/auth/AccountModal.test.tsx +++ b/src/components/auth/AccountModal.test.tsx @@ -219,8 +219,16 @@ test('account panel includes merged security devices and audit sections', async sessions: [ { sessionId: 'session-1', + clientType: 'mobile', + clientRuntime: 'ios', + clientPlatform: 'wechat', clientLabel: 'iPhone 15 Pro', + deviceDisplayName: 'iPhone 15 Pro / 微信', + miniProgramAppId: null, + miniProgramEnv: null, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)', isCurrent: true, + createdAt: '2026-04-20T07:30:00.000Z', lastSeenAt: '2026-04-20T09:00:00.000Z', expiresAt: '2026-04-27T09:00:00.000Z', ipMasked: '10.0.*.*', @@ -229,10 +237,12 @@ test('account panel includes merged security devices and audit sections', async auditLogs: [ { id: 'log-1', + eventType: 'phone_login', title: '登录成功', detail: '通过手机号验证码完成登录。', createdAt: '2026-04-20T08:00:00.000Z', ipMasked: '10.0.*.*', + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)', }, ], }); diff --git a/src/components/big-fish-creation/BigFishAgentWorkspace.tsx b/src/components/big-fish-creation/BigFishAgentWorkspace.tsx new file mode 100644 index 00000000..26feddd4 --- /dev/null +++ b/src/components/big-fish-creation/BigFishAgentWorkspace.tsx @@ -0,0 +1,102 @@ +import type { + BigFishAnchorItemResponse, + BigFishSessionSnapshotResponse, + ExecuteBigFishActionRequest, + SendBigFishMessageRequest, +} from '../../../packages/shared/src/contracts/bigFish'; +import { createCreationAgentClientMessageId } from '../../services/creation-agent'; +import { + type CreationAgentAnchorView, + type CreationAgentSessionView, + type CreationAgentTheme, + CreationAgentWorkspace, +} from '../creation-agent'; + +type BigFishAgentWorkspaceProps = { + session: BigFishSessionSnapshotResponse | null; + streamingReplyText?: string; + isBusy?: boolean; + error?: string | null; + onBack: () => void; + onSubmitMessage: (payload: SendBigFishMessageRequest) => void; + onExecuteAction: (payload: ExecuteBigFishActionRequest) => void; +}; + +const BIG_FISH_AGENT_THEME: CreationAgentTheme = { + accentTextClass: 'text-cyan-100/85', + accentBgClass: 'bg-cyan-200', + accentButtonClass: 'bg-cyan-200 shadow-cyan-950/20', + userBubbleClass: 'bg-cyan-600 text-white', + heroClass: + 'border border-cyan-100/16 bg-[radial-gradient(circle_at_top_left,rgba(45,212,191,0.22),transparent_32%),linear-gradient(135deg,rgba(8,47,73,0.96),rgba(13,24,38,0.96))]', + anchorGridClass: 'grid gap-2 sm:grid-cols-4', +}; + +function mapBigFishAnchor( + anchor: BigFishAnchorItemResponse, +): CreationAgentAnchorView { + return { + key: anchor.key, + label: anchor.label, + value: anchor.value, + status: anchor.status, + }; +} + +function mapBigFishSession( + session: BigFishSessionSnapshotResponse, +): CreationAgentSessionView { + return { + sessionId: session.sessionId, + title: '大鱼吃小鱼共创', + assistantSummary: + session.lastAssistantReply || + '先用一句灵感开始,Agent 会收束成可编译的玩法锚点。', + currentTurn: session.currentTurn, + progressPercent: session.progressPercent, + anchors: [ + session.anchorPack.gameplayPromise, + session.anchorPack.ecologyVisualTheme, + session.anchorPack.growthLadder, + session.anchorPack.riskTempo, + ].map(mapBigFishAnchor), + messages: session.messages, + recommendedReplies: [], + }; +} + +export function BigFishAgentWorkspace({ + session, + streamingReplyText = '', + isBusy = false, + error = null, + onBack, + onSubmitMessage, + onExecuteAction, +}: BigFishAgentWorkspaceProps) { + return ( + { + onSubmitMessage({ + clientMessageId: createCreationAgentClientMessageId('big-fish'), + text, + }); + }} + onPrimaryAction={() => { + onExecuteAction({ action: 'big_fish_compile_draft' }); + }} + /> + ); +} + +export default BigFishAgentWorkspace; diff --git a/src/components/big-fish-result/BigFishResultView.tsx b/src/components/big-fish-result/BigFishResultView.tsx new file mode 100644 index 00000000..da41b7af --- /dev/null +++ b/src/components/big-fish-result/BigFishResultView.tsx @@ -0,0 +1,471 @@ +import { + ArrowLeft, + CheckCircle2, + ImagePlus, + Loader2, + Play, + Sparkles, + Waves, +} from 'lucide-react'; +import { useMemo, useState } from 'react'; + +import type { + BigFishAssetSlotResponse, + BigFishGameDraftResponse, + BigFishLevelBlueprintResponse, + BigFishSessionSnapshotResponse, + ExecuteBigFishActionRequest, +} from '../../../packages/shared/src/contracts/bigFish'; + +type BigFishAssetStudioTarget = + | { + kind: 'level_main_image'; + level: BigFishLevelBlueprintResponse; + } + | { + kind: 'level_motion'; + level: BigFishLevelBlueprintResponse; + motionKey: 'idle_float' | 'move_swim'; + } + | { + kind: 'stage_background'; + }; + +type BigFishResultViewProps = { + session: BigFishSessionSnapshotResponse; + isBusy?: boolean; + error?: string | null; + onBack: () => void; + onExecuteAction: (payload: ExecuteBigFishActionRequest) => void; + onStartTestRun: () => void; +}; + +function findAssetSlot( + slots: BigFishAssetSlotResponse[], + assetKind: string, + level?: number, + motionKey?: string, +) { + return slots.find((slot) => { + if (slot.assetKind !== assetKind) { + return false; + } + if (level !== undefined && slot.level !== level) { + return false; + } + if (motionKey !== undefined && slot.motionKey !== motionKey) { + return false; + } + return true; + }); +} + +function assetReadyLabel(slot: BigFishAssetSlotResponse | undefined) { + return slot?.status === 'ready' ? '已生成' : '待生成'; +} + +function buildLevelAssetPreview(slot: BigFishAssetSlotResponse | undefined) { + if (slot?.assetUrl) { + return slot.assetUrl; + } + return null; +} + +function BigFishAssetStudioModal({ + draft, + target, + isBusy, + onClose, + onExecuteAction, +}: { + draft: BigFishGameDraftResponse; + target: BigFishAssetStudioTarget; + isBusy: boolean; + onClose: () => void; + onExecuteAction: (payload: ExecuteBigFishActionRequest) => void; +}) { + const title = + target.kind === 'stage_background' + ? '场地背景工坊' + : target.kind === 'level_main_image' + ? `Lv.${target.level.level} 主图工坊` + : `Lv.${target.level.level} 动作工坊`; + const prompt = + target.kind === 'stage_background' + ? draft.background.backgroundPromptSeed + : target.kind === 'level_main_image' + ? target.level.visualPromptSeed + : `${target.level.motionPromptSeed} / ${target.motionKey}`; + + const execute = () => { + if (target.kind === 'stage_background') { + onExecuteAction({ action: 'big_fish_generate_stage_background' }); + return; + } + + if (target.kind === 'level_main_image') { + onExecuteAction({ + action: 'big_fish_generate_level_main_image', + level: target.level.level, + }); + return; + } + + onExecuteAction({ + action: 'big_fish_generate_level_motion', + level: target.level.level, + motionKey: target.motionKey, + }); + }; + + return ( +
+
+
+
+ {title} +
+
+ {target.kind === 'stage_background' + ? draft.background.theme + : target.level.oneLineFantasy} +
+
+
+
+
+ PROMPT +
+
+ {prompt} +
+
+
+ AI 资产候选预览 +
+
+
+ + +
+
+
+ ); +} + +function BigFishLevelCard({ + level, + slots, + isBusy, + onOpenStudio, +}: { + level: BigFishLevelBlueprintResponse; + slots: BigFishAssetSlotResponse[]; + isBusy: boolean; + onOpenStudio: (target: BigFishAssetStudioTarget) => void; +}) { + const mainImageSlot = findAssetSlot( + slots, + 'level_main_image', + level.level, + ); + const idleSlot = findAssetSlot( + slots, + 'level_motion', + level.level, + 'idle_float', + ); + const moveSlot = findAssetSlot( + slots, + 'level_motion', + level.level, + 'move_swim', + ); + const previewUrl = buildLevelAssetPreview(mainImageSlot); + + return ( +
+
+
+ {previewUrl ? ( + {level.name} + ) : ( + + )} +
+
+
+
+
+ LV.{level.level} +
+
+ {level.name} +
+
+ {level.isFinalLevel ? ( + + 终局 + + ) : null} +
+
+ {level.oneLineFantasy} +
+
+ 猎物 {level.preyWindow.join('/') || '-'} + 威胁 {level.threatWindow.join('/') || '-'} + 主图 {assetReadyLabel(mainImageSlot)} + + 动作 {[assetReadyLabel(idleSlot), assetReadyLabel(moveSlot)].join('/')} + +
+
+
+
+ + + +
+
+ ); +} + +export function BigFishResultView({ + session, + isBusy = false, + error = null, + onBack, + onExecuteAction, + onStartTestRun, +}: BigFishResultViewProps) { + const [studioTarget, setStudioTarget] = + useState(null); + const draft = session.draft; + const backgroundSlot = findAssetSlot(session.assetSlots, 'stage_background'); + const blockers = useMemo( + () => session.assetCoverage.blockers.filter(Boolean), + [session.assetCoverage.blockers], + ); + + if (!draft) { + return ( +
+
+ 还没有可编辑的玩法草稿 +
+
+ ); + } + + return ( +
+
+
+ +
+ + +
+
+
+
+ {draft.title} +
+
+ {draft.subtitle} +
+
+
+ + {draft.coreFun} + + + {draft.ecologyTheme} + + + {draft.runtimeParams.levelCount} 级 + +
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+
+
+ {draft.levels.map((level) => ( + + ))} +
+
+ + +
+ + {studioTarget ? ( + { + setStudioTarget(null); + }} + onExecuteAction={(payload) => { + onExecuteAction(payload); + setStudioTarget(null); + }} + /> + ) : null} +
+ ); +} + +export default BigFishResultView; diff --git a/src/components/big-fish-runtime/BigFishRuntimeShell.tsx b/src/components/big-fish-runtime/BigFishRuntimeShell.tsx new file mode 100644 index 00000000..0032211e --- /dev/null +++ b/src/components/big-fish-runtime/BigFishRuntimeShell.tsx @@ -0,0 +1,230 @@ +import { ArrowLeft, Loader2 } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +import type { + BigFishRuntimeEntityResponse, + BigFishRuntimeSnapshotResponse, + SubmitBigFishInputRequest, +} from '../../../packages/shared/src/contracts/bigFish'; + +type BigFishRuntimeShellProps = { + run: BigFishRuntimeSnapshotResponse | null; + isBusy?: boolean; + error?: string | null; + onBack: () => void; + onSubmitInput: (payload: SubmitBigFishInputRequest) => void; +}; + +function clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); +} + +function normalizeVector(x: number, y: number) { + const length = Math.hypot(x, y); + if (length <= 0.001) { + return { x: 0, y: 0 }; + } + const capped = Math.min(1, length); + return { + x: (x / length) * capped, + y: (y / length) * capped, + }; +} + +function projectEntity( + entity: BigFishRuntimeEntityResponse, + run: BigFishRuntimeSnapshotResponse, +) { + const viewportWidth = 360; + const viewportHeight = 640; + const worldWidth = 420; + const worldHeight = 760; + const x = + viewportWidth / 2 + + ((entity.position.x - run.cameraCenter.x) / worldWidth) * viewportWidth; + const y = + viewportHeight / 2 + + ((entity.position.y - run.cameraCenter.y) / worldHeight) * viewportHeight; + + return { + left: `${clamp(x, -40, viewportWidth + 40)}px`, + top: `${clamp(y, -40, viewportHeight + 40)}px`, + width: `${Math.max(22, entity.radius * 2.2)}px`, + height: `${Math.max(22, entity.radius * 2.2)}px`, + }; +} + +function BigFishEntityDot({ + entity, + run, + owned, +}: { + entity: BigFishRuntimeEntityResponse; + run: BigFishRuntimeSnapshotResponse; + owned: boolean; +}) { + const projected = projectEntity(entity, run); + const isLeader = run.leaderEntityId === entity.entityId; + + return ( +
run.playerLevel + ? 'border-rose-100/70 bg-rose-500/88 shadow-rose-950/24' + : 'border-emerald-100/70 bg-emerald-400/88 shadow-emerald-950/20' + }`} + style={projected} + > + + {entity.level} + +
+ ); +} + +export function BigFishRuntimeShell({ + run, + isBusy = false, + error = null, + onBack, + onSubmitInput, +}: BigFishRuntimeShellProps) { + const padRef = useRef(null); + const [stick, setStick] = useState({ x: 0, y: 0 }); + const stickRef = useRef(stick); + + useEffect(() => { + stickRef.current = stick; + }, [stick]); + + useEffect(() => { + const timer = window.setInterval(() => { + const current = stickRef.current; + // 即使摇杆静止也持续回传当前输入,让后端持续推进刷怪、清理与胜负裁决。 + onSubmitInput(current); + }, 220); + + return () => { + window.clearInterval(timer); + }; + }, [onSubmitInput]); + + const updateStickFromPointer = (clientX: number, clientY: number) => { + const pad = padRef.current; + if (!pad) { + return; + } + const rect = pad.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const vector = normalizeVector( + (clientX - centerX) / (rect.width / 2), + (clientY - centerY) / (rect.height / 2), + ); + setStick(vector); + onSubmitInput(vector); + }; + + if (!run) { + return ( +
+
+ + 正在进入玩法 +
+
+ ); + } + + const statusLabel = + run.status === 'won' ? '通关' : run.status === 'failed' ? '失败' : '进行中'; + + return ( +
+
+
+ +
+ +
+ Lv.{run.playerLevel}/{run.winLevel} · {statusLabel} +
+
+ +
+ {run.wildEntities.map((entity) => ( + + ))} + {run.ownedEntities.map((entity) => ( + + ))} +
+ +
+
{ + event.currentTarget.setPointerCapture(event.pointerId); + updateStickFromPointer(event.clientX, event.clientY); + }} + onPointerMove={(event) => { + if (event.buttons <= 0) { + return; + } + updateStickFromPointer(event.clientX, event.clientY); + }} + onPointerUp={() => { + setStick({ x: 0, y: 0 }); + onSubmitInput({ x: 0, y: 0 }); + }} + onPointerCancel={() => { + setStick({ x: 0, y: 0 }); + onSubmitInput({ x: 0, y: 0 }); + }} + > +
+
+
+ +
+ {isBusy ?
同步中...
: null} + {error ?
{error}
: null} + {run.eventLog.slice(-3).map((event) => ( +
+ {event} +
+ ))} +
+
+
+ ); +} + +export default BigFishRuntimeShell; diff --git a/src/components/creation-agent/CreationAgentWorkspace.test.tsx b/src/components/creation-agent/CreationAgentWorkspace.test.tsx new file mode 100644 index 00000000..1ddebd93 --- /dev/null +++ b/src/components/creation-agent/CreationAgentWorkspace.test.tsx @@ -0,0 +1,112 @@ +/* @vitest-environment jsdom */ + +import { render, screen } from '@testing-library/react'; +import { afterEach, expect, test, vi } from 'vitest'; + +import { type CreationAgentTheme,CreationAgentWorkspace } from './CreationAgentWorkspace'; + +const testTheme: CreationAgentTheme = { + accentTextClass: 'text-emerald-100', + accentBgClass: 'bg-emerald-300', + accentButtonClass: 'bg-emerald-200', + userBubbleClass: 'bg-emerald-600 text-white', + heroClass: 'border border-emerald-100/20 bg-slate-900', +}; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('creation agent workspace filters duplicate recommended replies', () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = () => {}; + } + + render( + {}} + onSubmitText={() => {}} + onPrimaryAction={() => {}} + />, + ); + + expect(screen.getByRole('button', { name: '继续补充冲突' })).toBeTruthy(); + expect(screen.getByRole('button', { name: '先确定玩家身份' })).toBeTruthy(); + expect(screen.queryByRole('button', { name: /^\s*$/u })).toBeNull(); + + const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) => + call.some( + (arg) => + typeof arg === 'string' && + arg.includes('Encountered two children with the same key'), + ), + ); + + expect(duplicateKeyCalls).toHaveLength(0); +}); + +test('creation agent workspace renders streaming assistant text', () => { + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = () => {}; + } + + render( + {}} + onSubmitText={() => {}} + onPrimaryAction={() => {}} + />, + ); + + expect(screen.getByText(/那我先顺着这个方向收一下/u)).toBeTruthy(); +}); diff --git a/src/components/creation-agent/CreationAgentWorkspace.tsx b/src/components/creation-agent/CreationAgentWorkspace.tsx new file mode 100644 index 00000000..1ce8ad26 --- /dev/null +++ b/src/components/creation-agent/CreationAgentWorkspace.tsx @@ -0,0 +1,493 @@ +import { ArrowLeft, Send, Sparkles } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +import { + type CreationAgentProgressCopy, + normalizeCreationAgentProgress, + resolveCreationAgentProgressHint, + resolveCreationAnchorStatusLabel, +} from '../../services/creation-agent'; + +export type CreationAgentAnchorView = { + key: string; + label: string; + value: string; + status: string; +}; + +export type CreationAgentMessageView = { + id: string; + role: string; + kind?: string; + text: string; + createdAt?: string; +}; + +export type CreationAgentOperationView = { + operationId?: string; + type?: string; + status: string; + phaseLabel: string; + phaseDetail?: string; + progress: number; + error?: string | null; +}; + +export type CreationAgentSessionView = { + sessionId: string; + title: string; + assistantSummary?: string | null; + currentTurn: number; + progressPercent: number; + anchors: CreationAgentAnchorView[]; + messages: CreationAgentMessageView[]; + recommendedReplies?: string[]; +}; + +export type CreationAgentTheme = { + accentTextClass: string; + accentBgClass: string; + accentButtonClass: string; + userBubbleClass: string; + heroClass: string; + anchorGridClass?: string; +}; + +export type CreationAgentQuickAction = { + key: string; + label: string; + minTurn?: number; + minProgress?: number; + showWhenComplete?: boolean; +}; + +type CreationAgentWorkspaceProps = { + session: CreationAgentSessionView | null; + theme: CreationAgentTheme; + loadingText: string; + composerPlaceholder: string; + primaryActionLabel: string; + progressCopy?: CreationAgentProgressCopy; + activeOperation?: CreationAgentOperationView | null; + streamingReplyText?: string; + isStreamingReply?: boolean; + isBusy?: boolean; + error?: string | null; + quickActions?: CreationAgentQuickAction[]; + onBack: () => void; + onSubmitText: (text: string, quickActionKey?: string) => void; + onPrimaryAction: () => void; + onQuickAction?: (action: CreationAgentQuickAction) => void; +}; + +function uniqueRecommendedReplies(recommendedReplies: string[] = []) { + return [...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean))].slice( + 0, + 3, + ); +} + +function CreationAgentOperationBanner({ + operation, +}: { + operation: CreationAgentOperationView | null | undefined; +}) { + const [visibleOperation, setVisibleOperation] = + useState(operation ?? null); + + useEffect(() => { + setVisibleOperation(operation ?? null); + + if (operation?.status !== 'completed') { + return; + } + + const timeoutId = window.setTimeout(() => { + setVisibleOperation((current) => + current?.operationId === operation.operationId ? null : current, + ); + }, 1200); + + return () => window.clearTimeout(timeoutId); + }, [operation]); + + if (!visibleOperation) { + return null; + } + + const isFailed = visibleOperation.status === 'failed'; + const isRunning = + visibleOperation.status === 'running' || + visibleOperation.status === 'queued'; + const bannerToneClass = isFailed + ? 'platform-banner--danger' + : isRunning + ? 'platform-banner--info' + : 'platform-banner--success'; + const progress = normalizeCreationAgentProgress(visibleOperation.progress); + const progressFillStyle = isFailed + ? { background: 'linear-gradient(90deg, #fb7185 0%, #f43f5e 100%)' } + : isRunning + ? { background: 'var(--platform-button-primary-fill)' } + : { background: 'linear-gradient(90deg, #86efac 0%, #34d399 100%)' }; + + return ( +
+
+
+ {visibleOperation.phaseLabel} +
+
{progress}%
+
+ {visibleOperation.phaseDetail ? ( +
+ {visibleOperation.phaseDetail} +
+ ) : null} + {visibleOperation.error ? ( +
{visibleOperation.error}
+ ) : null} +
+
+
+
+ ); +} + +function CreationAgentMessageBubble({ + message, + theme, + recommendedReplies, + onRecommendedReply, +}: { + message: CreationAgentMessageView; + theme: CreationAgentTheme; + recommendedReplies?: string[]; + onRecommendedReply: (text: string) => void; +}) { + const isUser = message.role === 'user'; + const isSystem = message.role === 'system'; + const visibleRecommendedReplies = isUser + ? [] + : uniqueRecommendedReplies(recommendedReplies); + const bubbleToneClass = isUser + ? theme.userBubbleClass + : isSystem + ? 'border border-[var(--platform-warm-border)] bg-[var(--platform-warm-bg)] text-[var(--platform-warm-text)]' + : 'platform-subpanel text-[var(--platform-text-strong)]'; + + return ( +
+
+
{message.text}
+ {visibleRecommendedReplies.length > 0 ? ( +
+ {visibleRecommendedReplies.map((reply, replyIndex) => ( + + ))} +
+ ) : null} +
+
+ ); +} + +function CreationAgentAnchorChip({ + anchor, + theme, +}: { + anchor: CreationAgentAnchorView; + theme: CreationAgentTheme; +}) { + return ( +
+
+ + {anchor.label} + + + {resolveCreationAnchorStatusLabel(anchor.status)} + +
+
+ {anchor.value || '等待补齐'} +
+
+ ); +} + +function shouldShowQuickAction( + action: CreationAgentQuickAction, + session: CreationAgentSessionView, + progress: number, +) { + if (action.showWhenComplete && progress < 100) { + return false; + } + + if (!action.showWhenComplete && progress >= 100 && action.minProgress !== 100) { + return false; + } + + if (typeof action.minTurn === 'number' && session.currentTurn < action.minTurn) { + return false; + } + + if (typeof action.minProgress === 'number' && progress < action.minProgress) { + return false; + } + + return true; +} + +export function CreationAgentWorkspace({ + session, + theme, + loadingText, + composerPlaceholder, + primaryActionLabel, + progressCopy, + activeOperation = null, + streamingReplyText = '', + isStreamingReply = false, + isBusy = false, + error = null, + quickActions = [], + onBack, + onSubmitText, + onPrimaryAction, + onQuickAction, +}: CreationAgentWorkspaceProps) { + const [draftText, setDraftText] = useState(''); + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'end', + }); + }, [session?.messages, streamingReplyText, isStreamingReply]); + + if (!session) { + return ( +
+
+ {error || loadingText} +
+
+ ); + } + + const progress = normalizeCreationAgentProgress(session.progressPercent); + const visibleQuickActions = quickActions.filter((action) => + shouldShowQuickAction(action, session, progress), + ); + const lastAssistantMessageIndex = session.messages.reduce( + (lastIndex, message, index) => + message.role === 'assistant' ? index : lastIndex, + -1, + ); + + const submit = () => { + const text = draftText.trim(); + if (!text || isBusy) { + return; + } + + onSubmitText(text); + setDraftText(''); + }; + + return ( +
+
+
+ + +
+ +
+
+ {session.title} +
+ {session.assistantSummary ? ( +
+ {session.assistantSummary} +
+ ) : null} +
+ +
+
+ + 创作进度 + + + {progress}% + +
+
+
+
+
+ {resolveCreationAgentProgressHint(progress, progressCopy)} +
+
+ + {visibleQuickActions.length > 0 ? ( +
+ {visibleQuickActions.map((action) => ( + + ))} +
+ ) : null} +
+ + {session.anchors.length > 0 ? ( +
+ {session.anchors.map((anchor) => ( + + ))} +
+ ) : null} + + + +
+
+
+ {session.messages.length === 0 ? ( +
+ 暂无消息 +
+ ) : ( + session.messages.map((message, index) => ( + onSubmitText(text)} + /> + )) + )} + + {isStreamingReply ? ( +
+
+ {streamingReplyText ? ( +
+ {streamingReplyText} + +
+ ) : ( +
+ + + +
+ )} +
+
+ ) : null} +
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+
+