use super::*; pub(crate) fn map_big_fish_session_procedure_result( result: BigFishSessionProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let session = result .session .ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish session 快照"))?; Ok(map_big_fish_session_snapshot(session)) } pub(crate) fn map_big_fish_works_procedure_result( result: BigFishWorksProcedureResult, _fallback_owner_user_id: Option<&str>, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(result .items .into_iter() .map(map_big_fish_work_summary_snapshot) .collect()) } pub(crate) fn map_big_fish_run_procedure_result( result: BigFishRunProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let run = result .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish run 快照"))?; Ok(map_big_fish_runtime_snapshot(run)) } pub(crate) fn map_big_fish_session_snapshot( snapshot: BigFishSessionSnapshot, ) -> 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), } } pub(crate) fn map_big_fish_anchor_pack(snapshot: BigFishAnchorPack) -> 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), } } pub(crate) fn map_big_fish_anchor_item(snapshot: BigFishAnchorItem) -> BigFishAnchorItemRecord { BigFishAnchorItemRecord { key: snapshot.key, label: snapshot.label, value: snapshot.value, status: format_big_fish_anchor_status(snapshot.status).to_string(), } } pub(crate) fn map_big_fish_game_draft(snapshot: BigFishGameDraft) -> 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), } } pub(crate) fn map_big_fish_level_blueprint( snapshot: BigFishLevelBlueprint, ) -> BigFishLevelBlueprintRecord { BigFishLevelBlueprintRecord { level: snapshot.level, name: snapshot.name, one_line_fantasy: snapshot.one_line_fantasy, text_description: snapshot.text_description, silhouette_direction: snapshot.silhouette_direction, size_ratio: snapshot.size_ratio, visual_description: snapshot.visual_description, visual_prompt_seed: snapshot.visual_prompt_seed, idle_motion_description: snapshot.idle_motion_description, move_motion_description: snapshot.move_motion_description, 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, } } pub(crate) fn map_big_fish_background_blueprint( snapshot: BigFishBackgroundBlueprint, ) -> 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, } } pub(crate) fn map_big_fish_runtime_params( snapshot: BigFishRuntimeParams, ) -> 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, } } pub(crate) fn map_big_fish_asset_slot_snapshot( snapshot: BigFishAssetSlotSnapshot, ) -> 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), } } pub(crate) fn map_big_fish_asset_coverage( snapshot: BigFishAssetCoverage, ) -> 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, } } pub(crate) fn map_big_fish_agent_message_snapshot( snapshot: BigFishAgentMessageSnapshot, ) -> 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), } } pub(crate) fn map_big_fish_work_summary_snapshot( snapshot: BigFishWorkSummarySnapshot, ) -> BigFishWorkSummaryRecord { BigFishWorkSummaryRecord { work_id: snapshot.work_id, source_session_id: snapshot.source_session_id, owner_user_id: snapshot.owner_user_id, title: snapshot.title, subtitle: snapshot.subtitle, summary: snapshot.summary, cover_image_src: snapshot.cover_image_src, status: snapshot.status, updated_at_micros: snapshot.updated_at_micros, published_at_micros: snapshot.published_at_micros, publish_ready: snapshot.publish_ready, level_count: snapshot.level_count, 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, play_count: snapshot.play_count, remix_count: snapshot.remix_count, like_count: snapshot.like_count, recent_play_count_7d: snapshot.recent_play_count_7_d, } } pub(crate) fn map_big_fish_gallery_view_row( row: BigFishWorkSummarySnapshot, recent_play_count_7d: u32, ) -> BigFishWorkSummaryRecord { let mut record = map_big_fish_work_summary_snapshot(row); record.recent_play_count_7d = recent_play_count_7d; record } pub(crate) fn map_big_fish_runtime_snapshot( snapshot: BigFishRuntimeSnapshot, ) -> BigFishRuntimeRunRecord { BigFishRuntimeRunRecord { 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_snapshot) .collect(), wild_entities: snapshot .wild_entities .into_iter() .map(map_big_fish_runtime_entity_snapshot) .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( snapshot: BigFishRuntimeEntitySnapshot, ) -> 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: BigFishVector2) -> BigFishVector2Record { BigFishVector2Record { x: snapshot.x, y: snapshot.y, } } pub(crate) fn parse_big_fish_creation_stage( value: &str, ) -> Result { match value.trim() { "collecting_anchors" => Ok(BigFishCreationStage::CollectingAnchors), "draft_ready" => Ok(BigFishCreationStage::DraftReady), "asset_refining" => Ok(BigFishCreationStage::AssetRefining), "ready_to_publish" => Ok(BigFishCreationStage::ReadyToPublish), "published" => Ok(BigFishCreationStage::Published), other => Err(SpacetimeClientError::Runtime(format!( "big fish creation stage `{other}` 当前尚未支持" ))), } } pub(crate) fn format_big_fish_creation_stage(value: BigFishCreationStage) -> &'static str { match value { BigFishCreationStage::CollectingAnchors => "collecting_anchors", BigFishCreationStage::DraftReady => "draft_ready", BigFishCreationStage::AssetRefining => "asset_refining", BigFishCreationStage::ReadyToPublish => "ready_to_publish", BigFishCreationStage::Published => "published", } } pub(crate) fn format_big_fish_anchor_status(value: BigFishAnchorStatus) -> &'static str { match value { BigFishAnchorStatus::Confirmed => "confirmed", BigFishAnchorStatus::Inferred => "inferred", BigFishAnchorStatus::Missing => "missing", BigFishAnchorStatus::Locked => "locked", } } pub(crate) fn format_big_fish_agent_message_role(value: BigFishAgentMessageRole) -> &'static str { match value { BigFishAgentMessageRole::User => "user", BigFishAgentMessageRole::Assistant => "assistant", BigFishAgentMessageRole::System => "system", } } pub(crate) fn format_big_fish_agent_message_kind(value: BigFishAgentMessageKind) -> &'static str { match value { BigFishAgentMessageKind::Chat => "chat", BigFishAgentMessageKind::Summary => "summary", BigFishAgentMessageKind::ActionResult => "action_result", BigFishAgentMessageKind::Warning => "warning", } } pub(crate) fn format_big_fish_asset_kind(value: BigFishAssetKind) -> &'static str { match value { BigFishAssetKind::LevelMainImage => "level_main_image", BigFishAssetKind::LevelMotion => "level_motion", BigFishAssetKind::StageBackground => "stage_background", } } pub(crate) fn format_big_fish_asset_status(value: BigFishAssetStatus) -> &'static str { match value { BigFishAssetStatus::Missing => "missing", BigFishAssetStatus::Ready => "ready", } } pub(crate) fn format_big_fish_run_status(value: BigFishRunStatus) -> &'static str { match value { BigFishRunStatus::Running => "running", BigFishRunStatus::Won => "won", BigFishRunStatus::Failed => "failed", } } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BigFishWorkSummaryRecord { pub work_id: String, pub source_session_id: String, pub owner_user_id: String, pub title: String, pub subtitle: String, pub summary: String, pub cover_image_src: Option, pub status: String, pub updated_at_micros: i64, pub published_at_micros: Option, pub publish_ready: bool, pub level_count: u32, pub level_main_image_ready_count: u32, pub level_motion_ready_count: u32, pub background_ready: bool, pub play_count: u32, pub remix_count: u32, pub like_count: u32, pub recent_play_count_7d: u32, } #[cfg(test)] mod tests { use super::*; #[test] fn puzzle_works_mapper_keeps_typed_public_stat_fields() { let result = PuzzleWorksProcedureResult { ok: true, items: vec![PuzzleWorkProfile { work_id: "puzzle-work-1".to_string(), profile_id: "puzzle-profile-1".to_string(), owner_user_id: "user-1".to_string(), source_session_id: None, author_display_name: "测试作者".to_string(), work_title: "雨夜拼图作品".to_string(), work_description: "拼图作品说明".to_string(), level_name: "雨夜拼图".to_string(), summary: "公开作品摘要".to_string(), theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()], cover_image_src: None, cover_asset_id: None, levels: Vec::new(), publication_status: PuzzlePublicationStatus::Published, updated_at_micros: 123000000, published_at_micros: Some(123000000), play_count: 11, remix_count: 7, like_count: 5, recent_play_count_7_d: 3, point_incentive_total_half_points: 4, point_incentive_claimed_points: 2, publish_ready: true, anchor_pack: test_puzzle_anchor_pack(), }], error_message: None, }; let items = map_puzzle_works_procedure_result(result) .expect("typed puzzle works result 应能映射统计字段"); assert_eq!(items.len(), 1); assert_eq!(items[0].play_count, 11); assert_eq!(items[0].remix_count, 7); assert_eq!(items[0].like_count, 5); assert_eq!(items[0].recent_play_count_7d, 3); } #[test] fn puzzle_run_mapper_maps_typed_timer_fields() { let result = PuzzleRunProcedureResult { ok: true, run: Some(PuzzleRunSnapshot { run_id: "puzzle-run-1".to_string(), entry_profile_id: "puzzle-profile-1".to_string(), cleared_level_count: 0, current_level_index: 1, current_grid_size: 3, played_profile_ids: vec!["puzzle-profile-1".to_string()], previous_level_tags: vec![ "雨夜".to_string(), "猫咪".to_string(), "神庙".to_string(), ], current_level: Some(PuzzleRuntimeLevelSnapshot { run_id: "puzzle-run-1".to_string(), level_index: 1, level_id: None, grid_size: 3, profile_id: "puzzle-profile-1".to_string(), level_name: "雨夜拼图".to_string(), author_display_name: "测试作者".to_string(), theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()], cover_image_src: None, ui_background_image_src: None, ui_background_image_object_key: None, level_background_image_src: None, level_background_image_object_key: None, ui_spritesheet_image_src: None, ui_spritesheet_image_object_key: None, background_music: None, board: PuzzleBoardSnapshot { rows: 3, cols: 3, pieces: vec![PuzzlePieceState { piece_id: "piece-1".to_string(), correct_row: 0, correct_col: 0, current_row: 0, current_col: 0, merged_group_id: None, }], merged_groups: Vec::new(), selected_piece_id: None, all_tiles_resolved: false, }, status: PuzzleRuntimeLevelStatus::Playing, started_at_ms: 0, cleared_at_ms: None, elapsed_ms: None, time_limit_ms: 0, remaining_ms: 0, paused_accumulated_ms: 0, pause_started_at_ms: None, freeze_accumulated_ms: 0, freeze_started_at_ms: None, freeze_until_ms: None, leaderboard_entries: Vec::new(), }), recommended_next_profile_id: None, next_level_mode: "none".to_string(), next_level_profile_id: None, next_level_id: None, recommended_next_works: Vec::new(), leaderboard_entries: Vec::new(), }), error_message: None, }; let run = map_puzzle_run_procedure_result(result) .expect("typed puzzle run result 应能映射计时字段"); let level = run.current_level.expect("兼容后仍应保留当前关卡"); assert_eq!(run.run_id, "puzzle-run-1"); assert!(level.started_at_ms > 0); assert_eq!(level.time_limit_ms, 0); assert_eq!(level.remaining_ms, 0); assert!(level.leaderboard_entries.is_empty()); } #[test] fn big_fish_works_mapper_uses_typed_owner_and_public_stats() { let result = BigFishWorksProcedureResult { ok: true, items: vec![BigFishWorkSummarySnapshot { work_id: "big-fish-work-session-1".to_string(), source_session_id: "session-1".to_string(), owner_user_id: "user-1".to_string(), title: "深海草稿".to_string(), subtitle: "副标题".to_string(), summary: "摘要".to_string(), cover_image_src: None, status: "draft".to_string(), updated_at_micros: 123, publish_ready: false, level_count: 8, level_main_image_ready_count: 0, level_motion_ready_count: 0, background_ready: false, play_count: 9, remix_count: 4, like_count: 2, recent_play_count_7_d: 6, published_at_micros: None, }], error_message: None, }; let items = map_big_fish_works_procedure_result(result, Some("user-1")) .expect("typed big fish works result 应能映射 owner 和统计字段"); assert_eq!(items.len(), 1); assert_eq!(items[0].owner_user_id, "user-1"); assert_eq!(items[0].published_at_micros, None); assert_eq!(items[0].play_count, 9); assert_eq!(items[0].remix_count, 4); assert_eq!(items[0].like_count, 2); assert_eq!(items[0].recent_play_count_7d, 6); } #[test] fn match3d_work_mapper_keeps_generated_item_assets_json() { let result = Match3DWorkProcedureResult { ok: true, work: Some(Match3DWorkSnapshot { profile_id: "match3d-profile-1".to_string(), owner_user_id: "user-1".to_string(), source_session_id: "match3d-session-1".to_string(), author_display_name: "测试作者".to_string(), game_name: "水果抓大鹅".to_string(), theme_text: "水果".to_string(), summary_text: "水果主题".to_string(), tags: vec!["水果".to_string()], cover_image_src: String::new(), cover_asset_id: String::new(), clear_count: 3, difficulty: 3, config: Match3DCreatorConfigSnapshot { theme_text: "水果".to_string(), reference_image_src: None, clear_count: 3, difficulty: 3, asset_style_id: None, asset_style_label: None, asset_style_prompt: None, generate_click_sound: false, }, publication_status: "Draft".to_string(), publish_ready: false, play_count: 0, updated_at_micros: 123000000, published_at_micros: None, generated_item_assets_json: Some( r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"# .to_string(), ), }), error_message: None, }; let item = map_match3d_work_procedure_result(result) .expect("typed match3d work result 应保留生成素材 JSON"); assert_eq!( item.generated_item_assets_json.as_deref(), Some( r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"# ) ); } fn test_puzzle_anchor_pack() -> PuzzleAnchorPack { PuzzleAnchorPack { theme_promise: test_puzzle_anchor_item("themePromise", "题材承诺", "雨夜冒险"), visual_subject: test_puzzle_anchor_item("visualSubject", "画面主体", "猫咪神庙"), visual_mood: test_puzzle_anchor_item("visualMood", "视觉气质", "温暖"), composition_hooks: test_puzzle_anchor_item("compositionHooks", "拼图记忆点", "灯光"), tags_and_forbidden: test_puzzle_anchor_item( "tagsAndForbidden", "标签与禁忌", "雨夜, 猫咪, 神庙", ), } } fn test_puzzle_anchor_item(key: &str, label: &str, value: &str) -> PuzzleAnchorItem { PuzzleAnchorItem { key: key.to_string(), label: label.to_string(), value: value.to_string(), status: PuzzleAnchorStatus::Inferred, } } }