//! 自定义世界应用规则。 //! //! 这里只组合纯领域校验、草稿编译、profile 归一化和默认 JSON 结构,不承接外部副作用。 use crate::{commands::*, domain::*, errors::CustomWorldFieldError}; use serde_json::{Map, Value}; pub fn validate_custom_world_profile_fields( profile_id: &str, owner_user_id: &str, world_name: &str, profile_payload_json: &str, ) -> Result<(), CustomWorldFieldError> { if profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if world_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingWorldName); } if profile_payload_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfilePayloadJson); } Ok(()) } pub fn validate_custom_world_published_profile_compile_input( input: &CustomWorldPublishedProfileCompileInput, ) -> Result<(), CustomWorldFieldError> { if input.session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.draft_profile_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingDraftProfileJson); } if input.setting_text.trim().is_empty() { return Err(CustomWorldFieldError::MissingSettingText); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_publish_world_input( input: &CustomWorldPublishWorldInput, ) -> Result<(), CustomWorldFieldError> { if input.author_public_user_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } validate_custom_world_published_profile_compile_input( &CustomWorldPublishedProfileCompileInput { session_id: input.session_id.clone(), profile_id: input.profile_id.clone(), owner_user_id: input.owner_user_id.clone(), draft_profile_json: input.draft_profile_json.clone(), legacy_result_profile_json: input.legacy_result_profile_json.clone(), setting_text: input.setting_text.clone(), author_display_name: input.author_display_name.clone(), updated_at_micros: input.published_at_micros, }, ) } pub fn validate_custom_world_profile_upsert_input( input: &CustomWorldProfileUpsertInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_profile_fields( &input.profile_id, &input.owner_user_id, &input.world_name, &input.profile_payload_json, )?; if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_profile_publish_input( input: &CustomWorldProfilePublishInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } if input.author_public_user_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_profile_unpublish_input( input: &CustomWorldProfileUnpublishInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } Ok(()) } pub fn validate_custom_world_profile_delete_input( input: &CustomWorldProfileDeleteInput, ) -> Result<(), CustomWorldFieldError> { if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_profile_list_input( input: &CustomWorldProfileListInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } Ok(()) } pub fn validate_custom_world_library_detail_input( input: &CustomWorldLibraryDetailInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } Ok(()) } pub fn validate_custom_world_gallery_detail_input( input: &CustomWorldGalleryDetailInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if input.profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } Ok(()) } pub fn validate_custom_world_gallery_detail_by_code_input( input: &CustomWorldGalleryDetailByCodeInput, ) -> Result<(), CustomWorldFieldError> { if input.public_work_code.trim().is_empty() { return Err(CustomWorldFieldError::MissingPublicWorkCode); } Ok(()) } pub fn validate_custom_world_session_fields( session_id: &str, owner_user_id: &str, setting_text: &str, question_snapshot_json: &str, ) -> Result<(), CustomWorldFieldError> { if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if setting_text.trim().is_empty() { return Err(CustomWorldFieldError::MissingSettingText); } if question_snapshot_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingQuestionSnapshotJson); } Ok(()) } pub fn validate_custom_world_agent_session_fields( session_id: &str, owner_user_id: &str, anchor_content_json: &str, creator_intent_readiness_json: &str, pending_clarifications_json: &str, asset_coverage_json: &str, progress_percent: u32, ) -> Result<(), CustomWorldFieldError> { if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if anchor_content_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingAnchorContentJson); } if creator_intent_readiness_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingCreatorIntentReadinessJson); } if pending_clarifications_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingPendingClarificationsJson); } if asset_coverage_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingAssetCoverageJson); } if progress_percent > MAX_PROGRESS_PERCENT { return Err(CustomWorldFieldError::InvalidProgressPercent); } Ok(()) } pub fn validate_custom_world_agent_session_create_input( input: &CustomWorldAgentSessionCreateInput, ) -> Result<(), CustomWorldFieldError> { validate_custom_world_agent_session_fields( &input.session_id, &input.owner_user_id, &input.anchor_content_json, &input.creator_intent_readiness_json, &input.pending_clarifications_json, &input.asset_coverage_json, 0, )?; validate_custom_world_agent_message_fields( &input.welcome_message_id, &input.session_id, &input.welcome_message_text, )?; ensure_json_object(&input.anchor_content_json)?; ensure_optional_json_object(input.creator_intent_json.as_deref())?; ensure_json_object(&input.creator_intent_readiness_json)?; ensure_optional_json_object(input.anchor_pack_json.as_deref())?; ensure_optional_json_object(input.lock_state_json.as_deref())?; ensure_optional_json_object(input.draft_profile_json.as_deref())?; ensure_json_array(&input.pending_clarifications_json)?; ensure_json_array(&input.suggested_actions_json)?; ensure_json_array(&input.recommended_replies_json)?; ensure_json_array(&input.quality_findings_json)?; ensure_json_object(&input.asset_coverage_json)?; ensure_json_array(&input.checkpoints_json)?; Ok(()) } pub fn validate_custom_world_agent_session_get_input( input: &CustomWorldAgentSessionGetInput, ) -> 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); } Ok(()) } pub fn validate_custom_world_agent_message_submit_input( input: &CustomWorldAgentMessageSubmitInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } validate_custom_world_agent_message_fields( &input.user_message_id, &input.session_id, &input.user_message_text, )?; validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, "消息已处理", MAX_PROGRESS_PERCENT, )?; Ok(()) } pub fn validate_custom_world_agent_message_finalize_input( input: &CustomWorldAgentMessageFinalizeInput, ) -> Result<(), CustomWorldFieldError> { if input.owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } match input.operation_status { RpgAgentOperationStatus::Completed => { validate_custom_world_agent_message_fields( input.assistant_message_id.as_deref().unwrap_or_default(), &input.session_id, input.assistant_reply_text.as_deref().unwrap_or_default(), )?; } RpgAgentOperationStatus::Failed => {} _ => { validate_custom_world_agent_message_fields( input.assistant_message_id.as_deref().unwrap_or_default(), &input.session_id, input.assistant_reply_text.as_deref().unwrap_or_default(), )?; } } validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, &input.phase_label, input.operation_progress, )?; validate_custom_world_agent_session_fields( &input.session_id, &input.owner_user_id, &input.anchor_content_json, &input.creator_intent_readiness_json, &input.pending_clarifications_json, &input.asset_coverage_json, input.progress_percent, )?; ensure_json_object(&input.anchor_content_json)?; ensure_optional_json_object(input.creator_intent_json.as_deref())?; ensure_json_object(&input.creator_intent_readiness_json)?; ensure_optional_json_object(input.anchor_pack_json.as_deref())?; ensure_optional_json_object(input.draft_profile_json.as_deref())?; ensure_json_array(&input.pending_clarifications_json)?; ensure_json_array(&input.suggested_actions_json)?; ensure_json_array(&input.recommended_replies_json)?; ensure_json_array(&input.quality_findings_json)?; ensure_json_object(&input.asset_coverage_json)?; Ok(()) } pub fn validate_custom_world_agent_operation_get_input( input: &CustomWorldAgentOperationGetInput, ) -> 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.operation_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOperationId); } Ok(()) } pub fn validate_custom_world_agent_operation_progress_input( input: &CustomWorldAgentOperationProgressInput, ) -> 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(), })?; validate_custom_world_agent_operation_fields( &input.operation_id, &input.session_id, &input.phase_label, input.operation_progress, )?; 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); } ensure_optional_json_object(input.payload_json.as_deref())?; Ok(()) } pub fn validate_custom_world_agent_message_fields( message_id: &str, session_id: &str, text: &str, ) -> Result<(), CustomWorldFieldError> { if message_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingMessageId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if text.trim().is_empty() { return Err(CustomWorldFieldError::MissingMessageText); } Ok(()) } pub fn validate_custom_world_agent_operation_fields( operation_id: &str, session_id: &str, phase_label: &str, progress: u32, ) -> Result<(), CustomWorldFieldError> { if operation_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOperationId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if phase_label.trim().is_empty() { return Err(CustomWorldFieldError::MissingPhaseLabel); } if progress > MAX_PROGRESS_PERCENT { return Err(CustomWorldFieldError::InvalidProgressPercent); } Ok(()) } pub fn validate_custom_world_draft_card_fields( card_id: &str, session_id: &str, title: &str, summary: &str, linked_ids_json: &str, ) -> Result<(), CustomWorldFieldError> { if card_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardId); } if session_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingSessionId); } if title.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardTitle); } if summary.trim().is_empty() { return Err(CustomWorldFieldError::MissingCardSummary); } if linked_ids_json.trim().is_empty() { return Err(CustomWorldFieldError::MissingLinkedIdsJson); } Ok(()) } pub fn validate_custom_world_gallery_entry_fields( profile_id: &str, owner_user_id: &str, author_display_name: &str, world_name: &str, ) -> Result<(), CustomWorldFieldError> { if profile_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingProfileId); } if owner_user_id.trim().is_empty() { return Err(CustomWorldFieldError::MissingOwnerUserId); } if author_display_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingAuthorDisplayName); } if world_name.trim().is_empty() { return Err(CustomWorldFieldError::MissingWorldName); } Ok(()) } pub fn build_custom_world_published_profile_compile_snapshot( input: CustomWorldPublishedProfileCompileInput, ) -> Result { validate_custom_world_published_profile_compile_input(&input)?; let draft = parse_required_json_object( &input.draft_profile_json, CustomWorldFieldError::InvalidDraftProfileJson, )?; let legacy = parse_optional_json_object( input.legacy_result_profile_json.clone(), CustomWorldFieldError::InvalidLegacyResultProfileJson, )?; let world_name = resolve_text_field(&draft, &legacy, "name") .ok_or(CustomWorldFieldError::MissingWorldName)?; let subtitle = resolve_text_field(&draft, &legacy, "subtitle").unwrap_or_default(); let summary_text = resolve_text_field(&draft, &legacy, "summary").unwrap_or_default(); let cover_image_src = resolve_cover_image_src(&draft, &legacy); let theme_mode = resolve_theme_mode(&legacy); let playable_npc_count = count_distinct_roles(draft.get("playableNpcs"), draft.get("storyNpcs")); let landmark_count = to_array(draft.get("landmarks")).len() as u32; let compiled_payload_json = build_compiled_profile_payload_json( &input, &draft, &legacy, &world_name, &subtitle, &summary_text, )?; Ok(CustomWorldPublishedProfileCompileSnapshot { profile_id: input.profile_id, owner_user_id: input.owner_user_id, world_name, subtitle, summary_text, theme_mode, cover_image_src, playable_npc_count, landmark_count, author_display_name: input.author_display_name, compiled_profile_payload_json: compiled_payload_json, updated_at_micros: input.updated_at_micros, }) } pub fn canonicalize_custom_world_profile_before_save(profile: &mut Value) -> bool { let Some(object) = profile.as_object_mut() else { return false; }; let foundation_text = build_creator_intent_foundation_text(object.get("creatorIntent")) .trim() .to_string(); if foundation_text.is_empty() { return false; } let current_setting_text = object .get("settingText") .and_then(Value::as_str) .map(str::trim) .unwrap_or_default(); if current_setting_text == foundation_text { return false; } // 中文注释:保存与 session 同步前统一以后端 creatorIntent 锚点重建 settingText, // 避免浏览器继续持有正式 profile canonicalize 规则。 object.insert("settingText".to_string(), Value::String(foundation_text)); true } pub fn empty_agent_anchor_content_json() -> String { r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string() } pub fn empty_agent_creator_intent_readiness_json() -> String { r#"{"isReady":false,"completedKeys":[],"missingKeys":[]}"#.to_string() } pub fn empty_agent_asset_coverage_json() -> String { r#"{"roleAssets":[],"sceneAssets":[],"allRoleAssetsReady":false,"allSceneAssetsReady":false}"# .to_string() } pub fn empty_json_object() -> String { "{}".to_string() } pub fn empty_json_array() -> String { "[]".to_string() } pub fn normalize_optional_json_slice(value: Option) -> Option { value.and_then(|value| { let value = value.trim().to_string(); if value.is_empty() { None } else { Some(value) } }) } fn ensure_json_object(value: &str) -> Result<(), CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Object(_)) => Ok(()), _ => Err(CustomWorldFieldError::InvalidJsonPayload), } } fn ensure_optional_json_object(value: Option<&str>) -> Result<(), CustomWorldFieldError> { match value.map(str::trim).filter(|value| !value.is_empty()) { Some(value) => ensure_json_object(value), None => Ok(()), } } fn ensure_json_array(value: &str) -> Result<(), CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Array(_)) => Ok(()), _ => Err(CustomWorldFieldError::InvalidJsonPayload), } } fn parse_required_json_object( value: &str, error: CustomWorldFieldError, ) -> Result, CustomWorldFieldError> { match serde_json::from_str::(value) { Ok(Value::Object(object)) => Ok(object), _ => Err(error), } } fn parse_optional_json_object( value: Option, error: CustomWorldFieldError, ) -> Result, CustomWorldFieldError> { match normalize_optional_json_slice(value) { Some(value) => parse_required_json_object(&value, error), None => Ok(Map::new()), } } fn to_text(value: Option<&Value>) -> Option { match value { Some(Value::String(value)) => { let trimmed = value.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } } _ => None, } } fn to_array(value: Option<&Value>) -> Vec { match value { Some(Value::Array(items)) => items.clone(), _ => Vec::new(), } } fn to_object(value: Option<&Value>) -> Option> { match value { Some(Value::Object(object)) => Some(object.clone()), _ => None, } } fn build_creator_intent_foundation_text(value: Option<&Value>) -> String { let Some(intent) = value.and_then(Value::as_object) else { return String::new(); }; if !has_meaningful_creator_intent(intent) { return String::new(); } let relationship_text = intent .get("keyCharacters") .and_then(Value::as_array) .and_then(|items| items.first()) .and_then(Value::as_object) .map(build_creator_intent_relationship_text) .unwrap_or_default(); let player_opening_text = [ read_text(intent, "playerPremise"), read_text(intent, "openingSituation"), ] .into_iter() .flatten() .collect::>() .join(";"); let theme_tone_text = [ read_string_list(intent, "themeKeywords").join("、"), read_string_list(intent, "toneDirectives").join("、"), ] .into_iter() .filter(|value| !value.is_empty()) .collect::>() .join(" / "); [ build_anchor_line( "世界一句话", read_text(intent, "worldHook").unwrap_or_default(), ), build_anchor_line("玩家开局", player_opening_text), build_anchor_line("主题气质", theme_tone_text), build_anchor_line( "核心冲突", read_string_list(intent, "coreConflicts").join(";"), ), build_anchor_line("关键关系", relationship_text), build_anchor_line( "标志元素", read_string_list(intent, "iconicElements").join("、"), ), ] .into_iter() .filter(|value| !value.is_empty()) .collect::>() .join("\n") } fn has_meaningful_creator_intent(intent: &Map) -> bool { [ "rawSettingText", "worldHook", "playerPremise", "openingSituation", ] .iter() .any(|key| read_text(intent, key).is_some()) || [ "themeKeywords", "toneDirectives", "coreConflicts", "iconicElements", "forbiddenDirectives", ] .iter() .any(|key| !read_string_list(intent, key).is_empty()) || ["keyFactions", "keyCharacters", "keyLandmarks"] .iter() .any(|key| has_meaningful_creator_seed_array(intent.get(*key))) } fn build_creator_intent_relationship_text(character: &Map) -> String { [ read_text(character, "name"), read_text(character, "role"), read_text(character, "relationToPlayer").map(|value| format!("与玩家 {value}")), read_text(character, "hiddenHook").map(|value| format!("暗线 {value}")), ] .into_iter() .flatten() .collect::>() .join(" · ") } fn build_anchor_line(label: &str, content: String) -> String { if content.is_empty() { String::new() } else { format!("{label}:{content}") } } fn read_text(object: &Map, key: &str) -> Option { object .get(key) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } fn read_string_list(object: &Map, key: &str) -> Vec { object .get(key) .and_then(Value::as_array) .map(|items| { items .iter() .filter_map(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) .collect::>() }) .unwrap_or_default() } fn has_meaningful_creator_seed_array(value: Option<&Value>) -> bool { value.and_then(Value::as_array).is_some_and(|items| { items.iter().any(|item| { item.as_object().is_some_and(|object| { [ "name", "publicGoal", "tension", "notes", "role", "publicMask", "hiddenHook", "relationToPlayer", "purpose", "mood", "secret", ] .iter() .any(|key| read_text(object, key).is_some()) }) }) }) } fn resolve_text_field( draft: &Map, legacy: &Map, key: &str, ) -> Option { to_text(draft.get(key)).or_else(|| to_text(legacy.get(key))) } fn resolve_theme_mode(legacy: &Map) -> CustomWorldThemeMode { to_text(legacy.get("themeMode")) .and_then(|value| CustomWorldThemeMode::from_client_str(&value)) .unwrap_or(CustomWorldThemeMode::Mythic) } fn resolve_cover_image_src( draft: &Map, legacy: &Map, ) -> Option { if let Some(camp) = to_object(draft.get("camp")) { if let Some(image_src) = to_text(camp.get("imageSrc")) { return Some(image_src); } } for landmark in to_array(draft.get("landmarks")) { if let Value::Object(landmark) = landmark { if let Some(image_src) = to_text(landmark.get("imageSrc")) { return Some(image_src); } } } if let Some(cover) = to_object(legacy.get("cover")) { if let Some(image_src) = to_text(cover.get("imageSrc")) { return Some(image_src); } } to_text(legacy.get("coverImageSrc")) } fn count_distinct_roles(playable: Option<&Value>, story: Option<&Value>) -> u32 { let mut seen = std::collections::BTreeSet::new(); for role in to_array(playable).into_iter().chain(to_array(story)) { if let Value::Object(role) = role { let key = to_text(role.get("id")) .or_else(|| to_text(role.get("name"))) .unwrap_or_else(|| format!("role-{}", seen.len())); seen.insert(key); } } seen.len() as u32 } fn build_compiled_profile_payload_json( input: &CustomWorldPublishedProfileCompileInput, draft: &Map, legacy: &Map, world_name: &str, subtitle: &str, summary_text: &str, ) -> Result { let mut payload = legacy.clone(); payload.insert("id".to_string(), Value::String(input.profile_id.clone())); payload.insert( "settingText".to_string(), Value::String(input.setting_text.trim().to_string()), ); payload.insert("name".to_string(), Value::String(world_name.to_string())); payload.insert("subtitle".to_string(), Value::String(subtitle.to_string())); payload.insert( "summary".to_string(), Value::String(summary_text.to_string()), ); payload.insert( "updatedAtMicros".to_string(), Value::Number(input.updated_at_micros.into()), ); for key in ["tone", "playerGoal"] { if let Some(value) = draft.get(key) { payload.insert(key.to_string(), value.clone()); } } for key in [ "majorFactions", "coreConflicts", "playableNpcs", "storyNpcs", "landmarks", "camp", ] { if let Some(value) = draft.get(key) { payload.insert(key.to_string(), value.clone()); } } if let Some(scene_chapters) = draft .get("sceneChapterBlueprints") .or_else(|| draft.get("sceneChapters")) { payload.insert("sceneChapterBlueprints".to_string(), scene_chapters.clone()); } serde_json::to_string(&Value::Object(payload)) .map_err(|_| CustomWorldFieldError::InvalidDraftProfileJson) }