use super::*; impl From for CreationEntryTypeAdminUpsertInput { fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self { Self { id: input.id, title: input.title, subtitle: input.subtitle, badge: input.badge, image_src: input.image_src, visible: input.visible, open: input.open, sort_order: input.sort_order, category_id: input.category_id, category_label: input.category_label, category_sort_order: input.category_sort_order, unified_creation_spec_json: input.unified_creation_spec_json, } } } /// 将业务层 banner JSON 保存输入转换为 SpacetimeDB 生成绑定类型。 impl From for CreationEntryEventBannersAdminUpsertInput { fn from(input: module_runtime::CreationEntryEventBannersAdminUpsertInput) -> Self { Self { event_banners_json: input.event_banners_json, } } } impl From for AdminWorkVisibilityListInput { fn from(input: module_runtime::AdminWorkVisibilityListInput) -> Self { Self { admin_user_id: input.admin_user_id, } } } impl From for AdminWorkVisibilityUpdateInput { fn from(input: module_runtime::AdminWorkVisibilityUpdateInput) -> Self { Self { admin_user_id: input.admin_user_id, source_type: input.source_type, profile_id: input.profile_id, visible: input.visible, } } } pub(crate) fn build_admin_work_visibility_list_input( admin_user_id: String, ) -> Result { let admin_user_id = admin_user_id.trim().to_string(); if admin_user_id.is_empty() { return Err("adminUserId 不能为空".to_string()); } Ok(module_runtime::AdminWorkVisibilityListInput { admin_user_id }) } pub(crate) fn build_admin_work_visibility_update_input( admin_user_id: String, source_type: String, profile_id: String, visible: bool, ) -> Result { let admin_user_id = admin_user_id.trim().to_string(); if admin_user_id.is_empty() { return Err("adminUserId 不能为空".to_string()); } let source_type = source_type.trim().to_string(); if source_type.is_empty() { return Err("sourceType 不能为空".to_string()); } let profile_id = profile_id.trim().to_string(); if profile_id.is_empty() { return Err("profileId 不能为空".to_string()); } Ok(module_runtime::AdminWorkVisibilityUpdateInput { admin_user_id, source_type, profile_id, visible, }) } impl From for RuntimeSettingGetInput { fn from(input: module_runtime::RuntimeSettingGetInput) -> Self { Self { user_id: input.user_id, } } } impl From for RuntimeSettingUpsertInput { fn from(input: module_runtime::RuntimeSettingUpsertInput) -> Self { Self { user_id: input.user_id, music_volume: input.music_volume, platform_theme: map_runtime_platform_theme(input.platform_theme), updated_at_micros: input.updated_at_micros, } } } impl From for RuntimeBrowseHistoryListInput { fn from(input: module_runtime::RuntimeBrowseHistoryListInput) -> Self { Self { user_id: input.user_id, } } } impl From for RuntimeBrowseHistoryClearInput { fn from(input: module_runtime::RuntimeBrowseHistoryClearInput) -> Self { Self { user_id: input.user_id, } } } impl From for RuntimeBrowseHistorySyncInput { fn from(input: module_runtime::RuntimeBrowseHistorySyncInput) -> Self { Self { user_id: input.user_id, entries: input.entries.into_iter().map(Into::into).collect(), updated_at_micros: input.updated_at_micros, } } } impl From for RuntimeBrowseHistoryWriteInput { fn from(input: module_runtime::RuntimeBrowseHistoryWriteInput) -> Self { Self { owner_user_id: input.owner_user_id, profile_id: input.profile_id, world_name: input.world_name, subtitle: input.subtitle, summary_text: input.summary_text, cover_image_src: input.cover_image_src, theme_mode: input.theme_mode, author_display_name: input.author_display_name, visited_at: input.visited_at, } } } impl From for RuntimeSnapshotGetInput { fn from(input: module_runtime::RuntimeSnapshotGetInput) -> Self { Self { user_id: input.user_id, } } } impl From for RuntimeSnapshotDeleteInput { fn from(input: module_runtime::RuntimeSnapshotDeleteInput) -> Self { Self { user_id: input.user_id, } } } impl From for RuntimeTrackingEventInput { fn from(input: module_runtime::RuntimeTrackingEventInput) -> Self { Self { event_id: input.event_id, event_key: input.event_key, scope_kind: map_runtime_tracking_scope_kind(input.scope_kind), scope_id: input.scope_id, user_id: input.user_id, owner_user_id: input.owner_user_id, profile_id: input.profile_id, module_key: input.module_key, metadata_json: input.metadata_json, occurred_at_micros: input.occurred_at_micros, } } } pub type CreationEntryConfigRecord = shared_contracts::creation_entry_config::CreationEntryConfigResponse; pub type AdminWorkVisibilityRecord = shared_contracts::admin::AdminWorkVisibilityEntryPayload; pub(crate) fn map_creation_entry_config_procedure_result( result: CreationEntryConfigProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let snapshot = result .record .ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?; Ok(module_runtime::build_creation_entry_config_response( map_creation_entry_config_snapshot(snapshot), )) } pub(crate) fn map_admin_work_visibility_list_procedure_result( result: AdminWorkVisibilityListProcedureResult, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(result .entries .into_iter() .map(map_admin_work_visibility_snapshot) .collect()) } pub(crate) fn map_admin_work_visibility_procedure_result( result: AdminWorkVisibilityProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } result .record .map(map_admin_work_visibility_snapshot) .ok_or_else(|| SpacetimeClientError::missing_snapshot("后台作品可见性快照")) } fn map_admin_work_visibility_snapshot( snapshot: AdminWorkVisibilitySnapshot, ) -> AdminWorkVisibilityRecord { AdminWorkVisibilityRecord { source_type: snapshot.source_type, work_id: snapshot.work_id, profile_id: snapshot.profile_id, source_session_id: snapshot.source_session_id, public_work_code: snapshot.public_work_code, owner_user_id: snapshot.owner_user_id, author_display_name: snapshot.author_display_name, title: snapshot.title, subtitle: snapshot.subtitle, cover_image_src: snapshot.cover_image_src, visible: snapshot.visible, published_at_micros: snapshot.published_at_micros, updated_at_micros: snapshot.updated_at_micros, } } /// 从本地订阅表行组装创作入口配置响应,兼容旧单条 banner 字段。 pub(crate) fn build_creation_entry_config_record_from_rows( header: CreationEntryConfig, mut creation_types: Vec, ) -> CreationEntryConfigRecord { creation_types.sort_by(|left, right| { left.sort_order .cmp(&right.sort_order) .then_with(|| left.id.cmp(&right.id)) }); module_runtime::build_creation_entry_config_response( module_runtime::CreationEntryConfigSnapshot { config_id: header.config_id, start_card: module_runtime::CreationEntryStartCardSnapshot { title: header.start_title, description: header.start_description, idle_badge: header.start_idle_badge, busy_badge: header.start_busy_badge, }, type_modal: module_runtime::CreationEntryTypeModalSnapshot { title: header.modal_title, description: header.modal_description, }, event_banner: module_runtime::CreationEntryEventBannerSnapshot { title: creation_entry_text_or_default( header.event_title, module_runtime::DEFAULT_CREATION_ENTRY_EVENT_TITLE, ), description: creation_entry_text_or_default( header.event_description, module_runtime::DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION, ), cover_image_src: creation_entry_text_or_default( header.event_cover_image_src, module_runtime::DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC, ), prize_pool_mud_points: header.event_prize_pool_mud_points, starts_at_text: creation_entry_text_or_default( header.event_starts_at_text, module_runtime::DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT, ), ends_at_text: creation_entry_text_or_default( header.event_ends_at_text, module_runtime::DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT, ), render_mode: "structured".to_string(), html_code: None, }, event_banners_json: header.event_banners_json, creation_types: creation_types .into_iter() .map(|item| module_runtime::CreationEntryTypeSnapshot { id: item.id, title: item.title, subtitle: item.subtitle, badge: item.badge, image_src: item.image_src, visible: item.visible, open: item.open, sort_order: item.sort_order, category_id: creation_entry_text_or_default( item.category_id, module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_ID, ), category_label: creation_entry_text_or_default( item.category_label, module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_LABEL, ), category_sort_order: item.category_sort_order, updated_at_micros: item.updated_at.to_micros_since_unix_epoch(), unified_creation_spec_json: item.unified_creation_spec_json, }) .collect(), updated_at_micros: header.updated_at.to_micros_since_unix_epoch(), }, ) } /// 将 SpacetimeDB procedure 快照映射为 module-runtime 领域快照。 fn map_creation_entry_config_snapshot( snapshot: CreationEntryConfigSnapshot, ) -> module_runtime::CreationEntryConfigSnapshot { module_runtime::CreationEntryConfigSnapshot { config_id: snapshot.config_id, start_card: module_runtime::CreationEntryStartCardSnapshot { title: snapshot.start_card.title, description: snapshot.start_card.description, idle_badge: snapshot.start_card.idle_badge, busy_badge: snapshot.start_card.busy_badge, }, type_modal: module_runtime::CreationEntryTypeModalSnapshot { title: snapshot.type_modal.title, description: snapshot.type_modal.description, }, event_banner: module_runtime::CreationEntryEventBannerSnapshot { title: snapshot.event_banner.title, description: snapshot.event_banner.description, cover_image_src: snapshot.event_banner.cover_image_src, prize_pool_mud_points: snapshot.event_banner.prize_pool_mud_points, starts_at_text: snapshot.event_banner.starts_at_text, ends_at_text: snapshot.event_banner.ends_at_text, render_mode: snapshot.event_banner.render_mode, html_code: snapshot.event_banner.html_code, }, event_banners_json: snapshot.event_banners_json, creation_types: snapshot .creation_types .into_iter() .map(|item| module_runtime::CreationEntryTypeSnapshot { id: item.id, title: item.title, subtitle: item.subtitle, badge: item.badge, image_src: item.image_src, visible: item.visible, open: item.open, sort_order: item.sort_order, category_id: item.category_id, category_label: item.category_label, category_sort_order: item.category_sort_order, updated_at_micros: item.updated_at_micros, unified_creation_spec_json: item.unified_creation_spec_json, }) .collect(), updated_at_micros: snapshot.updated_at_micros, } } fn creation_entry_text_or_default(value: Option, default_value: &str) -> String { value .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| default_value.to_string()) } pub(crate) fn map_runtime_setting_procedure_result( result: RuntimeSettingProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } let snapshot = result .record .ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime settings 快照"))?; Ok(build_runtime_setting_record(map_runtime_setting_snapshot( snapshot, ))) } pub(crate) fn map_runtime_tracking_event_procedure_result( result: RuntimeTrackingEventProcedureResult, ) -> Result<(), SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(()) } pub(crate) fn map_runtime_tracking_event_batch_procedure_result( result: RuntimeTrackingEventBatchProcedureResult, ) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } Ok(result.accepted_count) } pub(crate) fn map_runtime_snapshot_procedure_result( result: RuntimeSnapshotProcedureResult, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } result .record .map(|snapshot| { build_runtime_snapshot_record(map_runtime_snapshot_snapshot(snapshot)) .map_err(|error| SpacetimeClientError::Runtime(error.to_string())) }) .transpose() } pub(crate) fn map_runtime_snapshot_required_procedure_result( result: RuntimeSnapshotProcedureResult, ) -> Result { map_runtime_snapshot_procedure_result(result)? .ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime snapshot 快照")) } pub(crate) fn map_runtime_snapshot_delete_procedure_result( result: RuntimeSnapshotProcedureResult, ) -> Result { map_runtime_snapshot_procedure_result(result).map(|record| record.is_some()) } pub(crate) fn map_runtime_setting_snapshot( snapshot: RuntimeSettingSnapshot, ) -> module_runtime::RuntimeSettingSnapshot { module_runtime::RuntimeSettingSnapshot { user_id: snapshot.user_id, music_volume: snapshot.music_volume, platform_theme: map_runtime_platform_theme_back(snapshot.platform_theme), created_at_micros: snapshot.created_at_micros, updated_at_micros: snapshot.updated_at_micros, } } pub(crate) fn map_runtime_platform_theme( value: DomainRuntimePlatformTheme, ) -> crate::module_bindings::RuntimePlatformTheme { match value { DomainRuntimePlatformTheme::Light => crate::module_bindings::RuntimePlatformTheme::Light, DomainRuntimePlatformTheme::Dark => crate::module_bindings::RuntimePlatformTheme::Dark, } } pub(crate) fn map_runtime_platform_theme_back( value: crate::module_bindings::RuntimePlatformTheme, ) -> DomainRuntimePlatformTheme { match value { crate::module_bindings::RuntimePlatformTheme::Light => DomainRuntimePlatformTheme::Light, crate::module_bindings::RuntimePlatformTheme::Dark => DomainRuntimePlatformTheme::Dark, } } pub(crate) fn map_runtime_tracking_scope_kind( value: DomainRuntimeTrackingScopeKind, ) -> crate::module_bindings::RuntimeTrackingScopeKind { match value { DomainRuntimeTrackingScopeKind::Site => { crate::module_bindings::RuntimeTrackingScopeKind::Site } DomainRuntimeTrackingScopeKind::Work => { crate::module_bindings::RuntimeTrackingScopeKind::Work } DomainRuntimeTrackingScopeKind::Module => { crate::module_bindings::RuntimeTrackingScopeKind::Module } DomainRuntimeTrackingScopeKind::User => { crate::module_bindings::RuntimeTrackingScopeKind::User } } } pub(crate) fn map_runtime_tracking_scope_kind_back( value: crate::module_bindings::RuntimeTrackingScopeKind, ) -> DomainRuntimeTrackingScopeKind { match value { crate::module_bindings::RuntimeTrackingScopeKind::Site => { DomainRuntimeTrackingScopeKind::Site } crate::module_bindings::RuntimeTrackingScopeKind::Work => { DomainRuntimeTrackingScopeKind::Work } crate::module_bindings::RuntimeTrackingScopeKind::Module => { DomainRuntimeTrackingScopeKind::Module } crate::module_bindings::RuntimeTrackingScopeKind::User => { DomainRuntimeTrackingScopeKind::User } } } pub(crate) fn parse_json_value( value: &str, label: &str, ) -> Result { serde_json::from_str::(value) .map_err(|error| SpacetimeClientError::Runtime(format!("{label} 非法: {error}"))) } pub(crate) fn parse_json_array( value: &str, label: &str, ) -> Result, SpacetimeClientError> { match parse_json_value(value, label)? { serde_json::Value::Array(entries) => Ok(entries), _ => Err(SpacetimeClientError::Runtime(format!( "{label} 必须是 JSON array" ))), } } pub(crate) fn parse_json_string_array( value: &str, label: &str, ) -> Result, SpacetimeClientError> { parse_json_array(value, label)? .into_iter() .map(|entry| match entry { serde_json::Value::String(value) => Ok(value), _ => Err(SpacetimeClientError::Runtime(format!( "{label} 必须是 string array" ))), }) .collect() } pub(crate) fn parse_supported_actions_json( value: &str, ) -> Result, SpacetimeClientError> { parse_json_array(value, "custom world agent supported_actions_json")? .into_iter() .map(|entry| { let object = entry.as_object().ok_or_else(|| { SpacetimeClientError::Runtime( "custom world supported action 必须是 JSON object".to_string(), ) })?; let action = object .get("action") .and_then(serde_json::Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .ok_or_else(|| { SpacetimeClientError::Runtime( "custom world supported action.action 缺失".to_string(), ) })?; let enabled = object .get("enabled") .and_then(serde_json::Value::as_bool) .ok_or_else(|| { SpacetimeClientError::Runtime( "custom world supported action.enabled 缺失".to_string(), ) })?; Ok(CustomWorldSupportedActionRecord { action: action.to_string(), enabled, reason: object .get("reason") .and_then(serde_json::Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), }) }) .collect() } #[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)] 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 BigFishRuntimeRunRecord { 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, }