Files
Genarrative/server-rs/crates/module-custom-world/src/application.rs

958 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 自定义世界应用规则。
//!
//! 这里只组合纯领域校验、草稿编译、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<CustomWorldPublishedProfileCompileSnapshot, CustomWorldFieldError> {
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<String>) -> Option<String> {
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>(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>(value) {
Ok(Value::Array(_)) => Ok(()),
_ => Err(CustomWorldFieldError::InvalidJsonPayload),
}
}
fn parse_required_json_object(
value: &str,
error: CustomWorldFieldError,
) -> Result<Map<String, Value>, CustomWorldFieldError> {
match serde_json::from_str::<Value>(value) {
Ok(Value::Object(object)) => Ok(object),
_ => Err(error),
}
}
fn parse_optional_json_object(
value: Option<String>,
error: CustomWorldFieldError,
) -> Result<Map<String, Value>, 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<String> {
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<Value> {
match value {
Some(Value::Array(items)) => items.clone(),
_ => Vec::new(),
}
}
fn to_object(value: Option<&Value>) -> Option<Map<String, Value>> {
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n")
}
fn has_meaningful_creator_intent(intent: &Map<String, Value>) -> 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, Value>) -> 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::<Vec<_>>()
.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<String, Value>, key: &str) -> Option<String> {
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<String, Value>, key: &str) -> Vec<String> {
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::<Vec<_>>()
})
.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<String, Value>,
legacy: &Map<String, Value>,
key: &str,
) -> Option<String> {
to_text(draft.get(key)).or_else(|| to_text(legacy.get(key)))
}
fn resolve_theme_mode(legacy: &Map<String, Value>) -> 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<String, Value>,
legacy: &Map<String, Value>,
) -> Option<String> {
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<String, Value>,
legacy: &Map<String, Value>,
world_name: &str,
subtitle: &str,
summary_text: &str,
) -> Result<String, CustomWorldFieldError> {
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)
}