use super::*; pub(crate) fn map_match3d_agent_session_procedure_result( result: Match3DAgentSessionProcedureResult, ) -> 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 未返回 match3d agent session 快照".to_string(), ) })?; Ok(map_match3d_agent_session_snapshot(session)) } pub(crate) fn map_match3d_work_procedure_result( result: Match3DWorkProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::Procedure( result .error_message .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), )); } let work = result.work.ok_or_else(|| { SpacetimeClientError::Procedure( "SpacetimeDB procedure 未返回 match3d work 快照".to_string(), ) })?; Ok(map_match3d_work_snapshot(work)) } pub(crate) fn map_match3d_works_procedure_result( result: Match3DWorksProcedureResult, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::Procedure( result .error_message .unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()), )); } Ok(result .items .into_iter() .map(map_match3d_work_snapshot) .collect()) } pub(crate) fn map_match3d_run_procedure_result( result: Match3DRunProcedureResult, ) -> 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 未返回 match3d run 快照".to_string()) })?; Ok(map_match3d_run_snapshot(run)) } pub(crate) fn map_match3d_click_item_procedure_result( result: Match3DClickItemProcedureResult, ) -> 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 未返回 match3d click run 快照".to_string(), ) })?; let run = map_match3d_run_snapshot(run); let accepted = result.status == "Accepted"; let accepted_item_instance_id = result.accepted_item_instance_id.clone(); let entered_slot_index = accepted_item_instance_id.as_deref().and_then(|item_id| { run.items .iter() .find(|item| item.item_instance_id == item_id) .and_then(|item| item.tray_slot_index) }); Ok(Match3DClickConfirmationRecord { status: result.status.clone(), accepted, reject_reason: if accepted { None } else { Some(result.status) }, accepted_item_instance_id, entered_slot_index, cleared_item_instance_ids: result.cleared_item_instance_ids, failure_reason: result.failure_reason, run, }) } fn map_match3d_agent_session_snapshot( snapshot: Match3DAgentSessionSnapshot, ) -> Match3DAgentSessionRecord { let config = map_match3d_creator_config(snapshot.config); Match3DAgentSessionRecord { session_id: snapshot.session_id, current_turn: snapshot.current_turn, progress_percent: snapshot.progress_percent, stage: normalize_match3d_stage(&snapshot.stage).to_string(), anchor_pack: build_match3d_anchor_pack(&config), draft: snapshot .draft .map(|draft| map_match3d_result_draft(draft, config.reference_image_src.clone())), config: Some(config), messages: snapshot .messages .into_iter() .map(map_match3d_agent_message_snapshot) .collect(), last_assistant_reply: empty_string_to_none(snapshot.last_assistant_reply), published_profile_id: snapshot.published_profile_id, updated_at: format_timestamp_micros(snapshot.updated_at_micros), } } fn map_match3d_creator_config( snapshot: Match3DCreatorConfigSnapshot, ) -> Match3DCreatorConfigRecord { Match3DCreatorConfigRecord { theme_text: snapshot.theme_text, reference_image_src: snapshot.reference_image_src, clear_count: snapshot.clear_count, difficulty: snapshot.difficulty, asset_style_id: snapshot.asset_style_id, asset_style_label: snapshot.asset_style_label, asset_style_prompt: snapshot.asset_style_prompt, generate_click_sound: snapshot.generate_click_sound, } } fn map_match3d_result_draft( snapshot: Match3DDraftSnapshot, reference_image_src: Option, ) -> Match3DResultDraftRecord { Match3DResultDraftRecord { profile_id: snapshot.profile_id, game_name: snapshot.game_name, theme_text: snapshot.theme_text, summary_text: snapshot.summary_text, tags: snapshot.tags, cover_image_src: None, reference_image_src, clear_count: snapshot.clear_count, difficulty: snapshot.difficulty, generated_item_assets_json: snapshot.generated_item_assets_json, total_item_count: snapshot.clear_count.saturating_mul(3), publish_ready: false, blockers: Vec::new(), } } fn map_match3d_agent_message_snapshot( snapshot: Match3DAgentMessageSnapshot, ) -> Match3DAgentMessageRecord { Match3DAgentMessageRecord { message_id: snapshot.message_id, role: snapshot.role, kind: normalize_match3d_message_kind(&snapshot.kind).to_string(), text: snapshot.text, created_at: format_timestamp_micros(snapshot.created_at_micros), } } fn map_match3d_work_snapshot(snapshot: Match3DWorkSnapshot) -> Match3DWorkProfileRecord { let config = map_match3d_creator_config(snapshot.config); Match3DWorkProfileRecord { work_id: snapshot.profile_id.clone(), profile_id: snapshot.profile_id, owner_user_id: snapshot.owner_user_id, source_session_id: empty_string_to_none(snapshot.source_session_id), author_display_name: snapshot.author_display_name, game_name: snapshot.game_name, theme_text: snapshot.theme_text, summary: snapshot.summary_text, tags: snapshot.tags, cover_image_src: empty_string_to_none(snapshot.cover_image_src), cover_asset_id: empty_string_to_none(snapshot.cover_asset_id), reference_image_src: config.reference_image_src, clear_count: snapshot.clear_count, difficulty: snapshot.difficulty, publication_status: normalize_match3d_publication_status(&snapshot.publication_status) .to_string(), play_count: snapshot.play_count, updated_at: format_timestamp_micros(snapshot.updated_at_micros), published_at: snapshot.published_at_micros.map(format_timestamp_micros), publish_ready: snapshot.publish_ready, generated_item_assets_json: snapshot.generated_item_assets_json, } } pub(crate) fn map_match3d_gallery_view_row(row: Match3DGalleryViewRow) -> Match3DWorkProfileRecord { Match3DWorkProfileRecord { work_id: row.profile_id.clone(), profile_id: row.profile_id, owner_user_id: row.owner_user_id, source_session_id: empty_string_to_none(row.source_session_id), author_display_name: row.author_display_name, game_name: row.game_name, theme_text: row.theme_text, summary: row.summary_text, tags: row.tags, cover_image_src: empty_string_to_none(row.cover_image_src), cover_asset_id: empty_string_to_none(row.cover_asset_id), reference_image_src: row.reference_image_src, clear_count: row.clear_count, difficulty: row.difficulty, publication_status: normalize_match3d_publication_status(&row.publication_status) .to_string(), play_count: row.play_count, updated_at: format_timestamp_micros(row.updated_at_micros), published_at: row.published_at_micros.map(format_timestamp_micros), publish_ready: row.publish_ready, generated_item_assets_json: row.generated_item_assets_json, } } fn map_match3d_run_snapshot(snapshot: Match3DRunSnapshot) -> Match3DRunRecord { let tray_slots = snapshot .tray_slots .into_iter() .map(map_match3d_tray_slot_snapshot) .collect::>(); let items = snapshot .items .into_iter() .map(|item| { let tray_slot_index = tray_slots .iter() .find(|slot| { slot.item_instance_id.as_deref() == Some(item.item_instance_id.as_str()) }) .map(|slot| slot.slot_index); map_match3d_item_snapshot(item, tray_slot_index) }) .collect(); Match3DRunRecord { run_id: snapshot.run_id, profile_id: snapshot.profile_id, owner_user_id: String::new(), status: snapshot.status, snapshot_version: u64::from(snapshot.snapshot_version), started_at_ms: i64_to_u64_ms(snapshot.started_at_ms), duration_limit_ms: i64_to_u64_ms(snapshot.duration_limit_ms), server_now_ms: Some(i64_to_u64_ms(snapshot.server_now_ms)), remaining_ms: i64_to_u64_ms(snapshot.remaining_ms), clear_count: snapshot.clear_count, total_item_count: snapshot.total_item_count, cleared_item_count: snapshot.cleared_item_count, items, tray_slots, failure_reason: snapshot.failure_reason, last_confirmed_action_id: None, } } fn map_match3d_item_snapshot( snapshot: Match3DItemSnapshot, tray_slot_index: Option, ) -> Match3DItemSnapshotRecord { Match3DItemSnapshotRecord { item_instance_id: snapshot.item_instance_id, item_type_id: snapshot.item_type_id, visual_key: snapshot.visual_key, x: snapshot.x, y: snapshot.y, radius: snapshot.radius, layer: snapshot.layer, state: snapshot.state, clickable: snapshot.clickable, tray_slot_index, } } fn map_match3d_tray_slot_snapshot(snapshot: Match3DTraySlotSnapshot) -> Match3DTraySlotRecord { Match3DTraySlotRecord { slot_index: snapshot.slot_index, item_instance_id: snapshot.item_instance_id, item_type_id: snapshot.item_type_id, visual_key: snapshot.visual_key, } } fn build_match3d_anchor_pack(config: &Match3DCreatorConfigRecord) -> Match3DAnchorPackRecord { let clear_count = config.clear_count.to_string(); let difficulty = config.difficulty.to_string(); Match3DAnchorPackRecord { theme: build_match3d_anchor_item("theme", "题材主题", config.theme_text.as_str()), clear_count: build_match3d_anchor_item("clearCount", "需要消除次数", clear_count.as_str()), difficulty: build_match3d_anchor_item("difficulty", "难度", difficulty.as_str()), } } fn build_match3d_anchor_item(key: &str, label: &str, value: &str) -> Match3DAnchorItemRecord { Match3DAnchorItemRecord { key: key.to_string(), label: label.to_string(), value: value.to_string(), status: if value.trim().is_empty() { "missing" } else { "confirmed" } .to_string(), } } fn normalize_match3d_stage(value: &str) -> &str { match value { "Collecting" | "collecting" | "collecting_config" => "collecting_config", "ReadyToCompile" | "ready_to_compile" => "ready_to_compile", "DraftCompiled" | "draft_compiled" | "draft_ready" => "draft_ready", "Published" | "published" => "published", _ => value, } } fn normalize_match3d_publication_status(value: &str) -> &str { match value { "Draft" | "draft" => "draft", "Published" | "published" => "published", _ => value, } } fn normalize_match3d_message_kind(value: &str) -> &str { match value { "text" => "chat", _ => value, } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAgentSessionCreateRecordInput { pub session_id: String, pub owner_user_id: String, pub seed_text: String, pub welcome_message_id: String, pub welcome_message_text: String, pub config_json: Option, pub created_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAgentMessageSubmitRecordInput { 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 Match3DAgentMessageFinalizeRecordInput { pub session_id: String, pub owner_user_id: String, pub assistant_message_id: Option, pub assistant_reply_text: Option, pub config_json: Option, pub progress_percent: u32, pub stage: String, pub updated_at_micros: i64, pub error_message: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DCompileDraftRecordInput { pub session_id: String, pub owner_user_id: String, pub profile_id: String, pub author_display_name: String, pub game_name: Option, pub summary_text: Option, pub tags_json: Option, pub cover_image_src: Option, pub cover_asset_id: Option, pub compiled_at_micros: i64, pub generated_item_assets_json: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DWorkUpdateRecordInput { pub profile_id: String, pub owner_user_id: String, pub game_name: String, pub theme_text: String, pub summary_text: String, pub tags_json: String, pub cover_image_src: String, pub cover_asset_id: String, pub clear_count: u32, pub difficulty: u32, pub updated_at_micros: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DRunStartRecordInput { pub run_id: String, pub owner_user_id: String, pub profile_id: String, pub started_at_ms: i64, pub item_type_count_override: u32, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DRunClickRecordInput { pub run_id: String, pub owner_user_id: String, pub item_instance_id: String, pub client_snapshot_version: u32, pub client_event_id: String, pub clicked_at_ms: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DRunStopRecordInput { pub run_id: String, pub owner_user_id: String, pub stopped_at_ms: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DRunRestartRecordInput { pub source_run_id: String, pub next_run_id: String, pub owner_user_id: String, pub restarted_at_ms: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DRunTimeUpRecordInput { pub run_id: String, pub owner_user_id: String, pub finished_at_ms: i64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAnchorItemRecord { pub key: String, pub label: String, pub value: String, pub status: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAnchorPackRecord { pub theme: Match3DAnchorItemRecord, pub clear_count: Match3DAnchorItemRecord, pub difficulty: Match3DAnchorItemRecord, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DCreatorConfigRecord { pub theme_text: String, pub reference_image_src: Option, pub clear_count: u32, pub difficulty: u32, pub asset_style_id: Option, pub asset_style_label: Option, pub asset_style_prompt: Option, pub generate_click_sound: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DResultDraftRecord { pub profile_id: String, pub game_name: String, pub theme_text: String, pub summary_text: String, pub tags: Vec, pub cover_image_src: Option, pub reference_image_src: Option, pub clear_count: u32, pub difficulty: u32, pub generated_item_assets_json: Option, pub total_item_count: u32, pub publish_ready: bool, pub blockers: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAgentMessageRecord { pub message_id: String, pub role: String, pub kind: String, pub text: String, pub created_at: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DAgentSessionRecord { pub session_id: String, pub current_turn: u32, pub progress_percent: u32, pub stage: String, pub anchor_pack: Match3DAnchorPackRecord, pub config: Option, pub draft: Option, pub messages: Vec, pub last_assistant_reply: Option, pub published_profile_id: Option, pub updated_at: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DWorkProfileRecord { pub work_id: String, pub profile_id: String, pub owner_user_id: String, pub source_session_id: Option, pub author_display_name: String, pub game_name: String, pub theme_text: String, pub summary: String, pub tags: Vec, pub cover_image_src: Option, pub cover_asset_id: Option, pub reference_image_src: Option, pub clear_count: u32, pub difficulty: u32, pub publication_status: String, pub play_count: u32, pub updated_at: String, pub published_at: Option, pub publish_ready: bool, pub generated_item_assets_json: Option, } #[derive(Clone, Debug, PartialEq)] pub struct Match3DItemSnapshotRecord { pub item_instance_id: String, pub item_type_id: String, pub visual_key: String, pub x: f32, pub y: f32, pub radius: f32, pub layer: u32, pub state: String, pub clickable: bool, pub tray_slot_index: Option, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Match3DTraySlotRecord { pub slot_index: u32, pub item_instance_id: Option, pub item_type_id: Option, pub visual_key: Option, } #[derive(Clone, Debug, PartialEq)] pub struct Match3DRunRecord { pub run_id: String, pub profile_id: String, pub owner_user_id: String, pub status: String, pub snapshot_version: u64, pub started_at_ms: u64, pub duration_limit_ms: u64, pub server_now_ms: Option, pub remaining_ms: u64, pub clear_count: u32, pub total_item_count: u32, pub cleared_item_count: u32, pub items: Vec, pub tray_slots: Vec, pub failure_reason: Option, pub last_confirmed_action_id: Option, } #[derive(Clone, Debug, PartialEq)] pub struct Match3DClickConfirmationRecord { pub status: String, pub accepted: bool, pub reject_reason: Option, pub accepted_item_instance_id: Option, pub entered_slot_index: Option, pub cleared_item_instance_ids: Vec, pub failure_reason: Option, pub run: Match3DRunRecord, }