use serde::{Deserialize, Serialize}; pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light"; pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark"; pub const SAVE_SNAPSHOT_VERSION: u32 = 2; pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync"; pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial"; pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane"; pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina"; pub const BROWSE_HISTORY_THEME_MODE_TIDE: &str = "tide"; pub const BROWSE_HISTORY_THEME_MODE_RIFT: &str = "rift"; pub const BROWSE_HISTORY_THEME_MODE_MYTHIC: &str = "mythic"; pub const CUSTOM_WORLD_VISIBILITY_DRAFT: &str = "draft"; pub const CUSTOM_WORLD_VISIBILITY_PUBLISHED: &str = "published"; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeSettingsResponse { pub music_volume: f32, pub platform_theme: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PutRuntimeSettingsRequest { pub music_volume: f32, pub platform_theme: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SavedGameSnapshotResponse { pub version: u32, pub saved_at: String, pub game_state: serde_json::Value, pub bottom_tab: String, pub current_story: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PutSavedGameSnapshotRequest { pub game_state: serde_json::Value, pub bottom_tab: String, #[serde(default)] pub current_story: Option, #[serde(default)] pub saved_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct BasicOkResponse { pub ok: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlatformBrowseHistoryEntryResponse { pub owner_user_id: String, pub profile_id: String, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub theme_mode: String, pub author_display_name: String, pub visited_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlatformBrowseHistoryWriteEntryRequest { pub owner_user_id: String, pub profile_id: String, pub world_name: String, #[serde(default)] pub subtitle: Option, #[serde(default)] pub summary_text: Option, #[serde(default)] pub cover_image_src: Option, #[serde(default)] pub theme_mode: Option, #[serde(default)] pub author_display_name: Option, #[serde(default)] pub visited_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlatformBrowseHistoryBatchSyncRequest { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum PlatformBrowseHistoryUpsertRequest { Single(PlatformBrowseHistoryWriteEntryRequest), Batch(PlatformBrowseHistoryBatchSyncRequest), } impl PlatformBrowseHistoryUpsertRequest { pub fn into_entries(self) -> Vec { match self { Self::Single(entry) => vec![entry], Self::Batch(batch) => batch.entries, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlatformBrowseHistoryResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileDashboardSummaryResponse { pub wallet_balance: u64, pub total_play_time_ms: u64, pub played_world_count: u32, pub updated_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileWalletLedgerEntryResponse { pub id: String, pub amount_delta: i64, pub balance_after: u64, pub source_type: String, pub created_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileWalletLedgerResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfilePlayedWorkSummaryResponse { pub world_key: String, pub owner_user_id: Option, pub profile_id: Option, pub world_type: Option, pub world_title: String, pub world_subtitle: String, pub first_played_at: String, pub last_played_at: String, pub last_observed_play_time_ms: u64, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfilePlayStatsResponse { pub total_play_time_ms: u64, pub played_works: Vec, pub updated_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveSummaryResponse { pub world_key: String, pub owner_user_id: Option, pub profile_id: Option, pub world_type: Option, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub last_played_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveListResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveResumeResponse { pub entry: ProfileSaveArchiveSummaryResponse, pub snapshot: SavedGameSnapshotResponse, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeInventorySlotResponse { pub slot_id: String, pub container_kind: String, pub slot_key: String, pub item_id: String, pub category: String, pub name: String, pub description: Option, pub quantity: u32, pub rarity: String, pub tags: Vec, pub stackable: bool, pub stack_key: String, pub equipment_slot_id: Option, pub source_kind: String, pub source_reference_id: Option, pub created_at: String, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeInventoryStateResponse { pub runtime_session_id: String, pub actor_user_id: String, pub backpack_items: Vec, pub equipment_items: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldProfileUpsertRequest { pub profile: serde_json::Value, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldLibraryEntryResponse { pub owner_user_id: String, pub profile_id: String, pub profile: serde_json::Value, pub visibility: String, pub published_at: Option, pub updated_at: String, pub author_display_name: String, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub theme_mode: String, pub playable_npc_count: u32, pub landmark_count: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldGalleryCardResponse { pub owner_user_id: String, pub profile_id: String, pub visibility: String, pub published_at: Option, pub updated_at: String, pub author_display_name: String, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub theme_mode: String, pub playable_npc_count: u32, pub landmark_count: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldLibraryResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldLibraryMutationResponse { pub entry: CustomWorldLibraryEntryResponse, pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldGalleryResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldGalleryDetailResponse { pub entry: CustomWorldLibraryEntryResponse, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldWorkSummaryResponse { 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, #[serde(default)] pub cover_render_mode: Option, #[serde(default)] pub cover_character_image_srcs: Vec, pub updated_at: String, pub published_at: 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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldWorksResponse { pub items: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreateCustomWorldAgentSessionRequest { #[serde(default)] pub seed_text: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SendCustomWorldAgentMessageRequest { pub client_message_id: String, pub text: String, #[serde(default)] pub quick_fill_requested: Option, #[serde(default)] pub focus_card_id: Option, #[serde(default)] pub selected_card_ids: Option>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentMessageResponse { pub id: String, pub role: String, pub kind: String, pub text: String, pub created_at: String, pub related_operation_id: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentOperationResponse { pub operation_id: String, #[serde(rename = "type")] pub operation_type: String, pub status: String, pub phase_label: String, pub phase_detail: String, pub progress: u32, pub error: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldDraftCardSummaryResponse { pub id: String, pub kind: String, pub title: String, pub subtitle: String, pub summary: String, pub status: String, pub linked_ids: Vec, pub warning_count: u32, pub asset_status: Option, pub asset_status_label: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldDraftCardDetailSectionResponse { pub id: String, pub label: String, pub value: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldDraftCardDetailResponse { pub id: String, pub kind: String, pub title: String, pub sections: Vec, pub linked_ids: Vec, pub locked: bool, pub editable: bool, pub editable_section_ids: Vec, pub warning_messages: Vec, pub asset_status: Option, pub asset_status_label: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentCardDetailResponse { pub card: CustomWorldDraftCardDetailResponse, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentCheckpointResponse { pub checkpoint_id: String, pub created_at: String, pub label: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldSupportedActionResponse { pub action: String, pub enabled: bool, pub reason: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldResultPreviewBlockerResponse { pub id: String, pub code: String, pub message: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldPublishGateResponse { pub profile_id: String, pub blockers: Vec, pub blocker_count: u32, pub publish_ready: bool, pub can_enter_world: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentSessionSnapshotResponse { pub session_id: String, pub current_turn: u32, pub anchor_content: serde_json::Value, pub progress_percent: u32, pub last_assistant_reply: Option, pub stage: String, pub focus_card_id: Option, pub creator_intent: serde_json::Value, pub creator_intent_readiness: serde_json::Value, pub anchor_pack: serde_json::Value, pub lock_state: serde_json::Value, pub draft_profile: serde_json::Value, pub messages: Vec, pub draft_cards: Vec, pub pending_clarifications: Vec, pub suggested_actions: Vec, pub recommended_replies: Vec, pub quality_findings: Vec, pub asset_coverage: serde_json::Value, pub checkpoints: Vec, pub supported_actions: Vec, pub publish_gate: Option, pub result_preview: Option, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldAgentSessionResponse { pub session: CustomWorldAgentSessionSnapshotResponse, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ExecuteCustomWorldAgentActionRequest { pub action: String, #[serde(default)] pub profile_id: Option, #[serde(default)] pub draft_profile: Option, #[serde(default)] pub legacy_result_profile: Option, #[serde(default)] pub setting_text: Option, #[serde(default)] pub card_id: Option, #[serde(default)] pub sections: Option>, #[serde(default)] pub profile: Option, #[serde(default)] pub count: Option, #[serde(default)] pub prompt_text: Option, #[serde(default)] pub anchor_card_ids: Option>, #[serde(default)] pub role_ids: Option>, #[serde(default)] pub role_id: Option, #[serde(default)] pub portrait_path: Option, #[serde(default)] pub generated_visual_asset_id: Option, #[serde(default)] pub generated_animation_set_id: Option, #[serde(default)] pub animation_map: Option, #[serde(default)] pub scene_ids: Option>, #[serde(default)] pub scene_id: Option, #[serde(default)] pub scene_kind: Option, #[serde(default)] pub image_src: Option, #[serde(default)] pub generated_scene_asset_id: Option, #[serde(default)] pub generated_scene_prompt: Option, #[serde(default)] pub generated_scene_model: Option, #[serde(default)] pub checkpoint_id: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ExecuteCustomWorldAgentDraftCardSectionPatch { pub section_id: String, pub value: String, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn runtime_settings_request_uses_camel_case_fields() { let payload = serde_json::to_value(PutRuntimeSettingsRequest { music_volume: 0.42, platform_theme: RUNTIME_PLATFORM_THEME_LIGHT.to_string(), }) .expect("payload should serialize"); assert_eq!(payload["platformTheme"], json!("light")); let music_volume = payload["musicVolume"] .as_f64() .expect("musicVolume should serialize as number"); assert!((music_volume - 0.42).abs() < 0.0001); } #[test] fn browse_history_response_uses_camel_case_fields() { let payload = serde_json::to_value(PlatformBrowseHistoryResponse { entries: vec![PlatformBrowseHistoryEntryResponse { owner_user_id: "owner-1".to_string(), profile_id: "profile-1".to_string(), world_name: "世界".to_string(), subtitle: "".to_string(), summary_text: "".to_string(), cover_image_src: None, theme_mode: BROWSE_HISTORY_THEME_MODE_MYTHIC.to_string(), author_display_name: "玩家".to_string(), visited_at: "2026-04-21T00:00:00Z".to_string(), }], }) .expect("payload should serialize"); assert_eq!(payload["entries"][0]["ownerUserId"], json!("owner-1")); assert_eq!(payload["entries"][0]["themeMode"], json!("mythic")); assert_eq!( payload["entries"][0]["visitedAt"], json!("2026-04-21T00:00:00Z") ); } #[test] fn browse_history_upsert_request_accepts_single_or_batch_shape() { let single: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({ "ownerUserId": "owner-1", "profileId": "profile-1", "worldName": "世界" })) .expect("single shape should deserialize"); let batch: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({ "entries": [{ "ownerUserId": "owner-1", "profileId": "profile-1", "worldName": "世界" }] })) .expect("batch shape should deserialize"); assert_eq!(single.into_entries().len(), 1); assert_eq!(batch.into_entries().len(), 1); } #[test] fn profile_dashboard_response_uses_camel_case_fields() { let payload = serde_json::to_value(ProfileDashboardSummaryResponse { wallet_balance: 8, total_play_time_ms: 16, played_world_count: 3, updated_at: Some("2026-04-22T10:00:00Z".to_string()), }) .expect("payload should serialize"); assert_eq!(payload["walletBalance"], json!(8)); assert_eq!(payload["totalPlayTimeMs"], json!(16)); assert_eq!(payload["playedWorldCount"], json!(3)); assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z")); } #[test] fn profile_wallet_ledger_response_uses_camel_case_fields() { let payload = serde_json::to_value(ProfileWalletLedgerResponse { entries: vec![ProfileWalletLedgerEntryResponse { id: "ledger-1".to_string(), amount_delta: 12, balance_after: 80, source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(), created_at: "2026-04-22T10:00:00Z".to_string(), }], }) .expect("payload should serialize"); assert_eq!(payload["entries"][0]["amountDelta"], json!(12)); assert_eq!(payload["entries"][0]["balanceAfter"], json!(80)); assert_eq!(payload["entries"][0]["sourceType"], json!("snapshot_sync")); assert_eq!( payload["entries"][0]["createdAt"], json!("2026-04-22T10:00:00Z") ); } #[test] fn profile_play_stats_response_uses_camel_case_fields() { let payload = serde_json::to_value(ProfilePlayStatsResponse { total_play_time_ms: 18, played_works: vec![ProfilePlayedWorkSummaryResponse { world_key: "builtin:WUXIA".to_string(), owner_user_id: None, profile_id: None, world_type: Some("WUXIA".to_string()), world_title: "武侠世界".to_string(), world_subtitle: "".to_string(), first_played_at: "2026-04-20T10:00:00Z".to_string(), last_played_at: "2026-04-22T10:00:00Z".to_string(), last_observed_play_time_ms: 1200, }], updated_at: Some("2026-04-22T10:00:00Z".to_string()), }) .expect("payload should serialize"); assert_eq!(payload["totalPlayTimeMs"], json!(18)); assert_eq!( payload["playedWorks"][0]["worldKey"], json!("builtin:WUXIA") ); assert_eq!( payload["playedWorks"][0]["lastObservedPlayTimeMs"], json!(1200) ); assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z")); } #[test] fn runtime_inventory_state_response_uses_camel_case_fields() { let payload = serde_json::to_value(RuntimeInventoryStateResponse { runtime_session_id: "runtime_001".to_string(), actor_user_id: "user_001".to_string(), backpack_items: vec![RuntimeInventorySlotResponse { slot_id: "invslot_001".to_string(), container_kind: "backpack".to_string(), slot_key: "invslot_001".to_string(), item_id: "consumable_heal_potion".to_string(), category: "消耗品".to_string(), name: "疗伤药".to_string(), description: Some("用于恢复少量气血。".to_string()), quantity: 2, rarity: "common".to_string(), tags: vec!["healing".to_string()], stackable: true, stack_key: "heal_potion".to_string(), equipment_slot_id: None, source_kind: "treasure_reward".to_string(), source_reference_id: Some("treasure_001".to_string()), created_at: "2026-04-22T10:00:00Z".to_string(), updated_at: "2026-04-22T10:01:00Z".to_string(), }], equipment_items: vec![], }) .expect("payload should serialize"); assert_eq!(payload["runtimeSessionId"], json!("runtime_001")); assert_eq!(payload["actorUserId"], json!("user_001")); assert_eq!(payload["backpackItems"][0]["slotId"], json!("invslot_001")); assert_eq!( payload["backpackItems"][0]["sourceKind"], json!("treasure_reward") ); } }