refactor: split large modules and normalize rust layout

This commit is contained in:
kdletters
2026-05-18 19:40:14 +08:00
parent 472a47eae7
commit 269f35cecf
51 changed files with 17492 additions and 17169 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,881 @@
use super::*;
pub(super) async fn submit_and_finalize_match3d_message(
state: &AppState,
request_context: &RequestContext,
owner_user_id: &str,
session_id: String,
payload: SendMatch3DAgentMessageRequest,
) -> Result<Match3DAgentSessionRecord, Response> {
ensure_non_empty(
request_context,
MATCH3D_AGENT_PROVIDER,
&session_id,
"sessionId",
)?;
ensure_non_empty(
request_context,
MATCH3D_AGENT_PROVIDER,
&payload.client_message_id,
"clientMessageId",
)?;
ensure_non_empty(
request_context,
MATCH3D_AGENT_PROVIDER,
&payload.text,
"text",
)?;
let submitted = state
.spacetime_client()
.submit_match3d_agent_message(Match3DAgentMessageSubmitRecordInput {
session_id: session_id.clone(),
owner_user_id: owner_user_id.to_string(),
user_message_id: payload.client_message_id.clone(),
user_message_text: payload.text.clone(),
submitted_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
map_match3d_client_error(error),
)
})?;
let next_turn = submitted.current_turn.saturating_add(1);
let next_config = build_config_from_message(&submitted, &payload);
let assistant_reply = build_match3d_assistant_reply_for_turn(&next_config, next_turn);
let progress_percent = resolve_progress_percent_for_turn(next_turn);
let stage = if progress_percent >= 100 {
"ReadyToCompile"
} else {
"Collecting"
}
.to_string();
state
.spacetime_client()
.finalize_match3d_agent_message(Match3DAgentMessageFinalizeRecordInput {
session_id,
owner_user_id: owner_user_id.to_string(),
assistant_message_id: Some(build_prefixed_uuid_id(MATCH3D_MESSAGE_ID_PREFIX)),
assistant_reply_text: Some(assistant_reply),
config_json: serialize_match3d_config(&next_config),
progress_percent,
stage,
updated_at_micros: current_utc_micros(),
error_message: None,
})
.await
.map_err(|error| {
match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
map_match3d_client_error(error),
)
})
}
pub(super) async fn load_match3d_agent_session_response_with_persisted_assets(
state: &AppState,
owner_user_id: &str,
session: Match3DAgentSessionRecord,
) -> Match3DAgentSessionSnapshotResponse {
let Some(profile_id) = resolve_match3d_session_existing_profile_id(&session) else {
return map_match3d_agent_session_response(session);
};
let assets =
get_match3d_existing_generated_item_assets(state, owner_user_id, profile_id.as_str()).await;
map_match3d_agent_session_response_with_assets(session, &assets)
}
fn resolve_match3d_session_existing_profile_id(
session: &Match3DAgentSessionRecord,
) -> Option<String> {
session
.draft
.as_ref()
.map(|draft| draft.profile_id.trim())
.filter(|profile_id| !profile_id.is_empty())
.or_else(|| {
session
.published_profile_id
.as_deref()
.map(str::trim)
.filter(|profile_id| !profile_id.is_empty())
})
.map(str::to_string)
}
pub(super) async fn compile_match3d_draft_for_session(
state: &AppState,
request_context: &RequestContext,
authenticated: &AuthenticatedAccessToken,
session_id: String,
game_name: Option<String>,
summary: Option<String>,
tags: Option<Vec<String>>,
cover_image_src: Option<String>,
generate_click_sound: Option<bool>,
) -> Result<(Match3DAgentSessionRecord, Vec<Match3DGeneratedItemAsset>), Response> {
let owner_user_id = authenticated.claims().user_id().to_string();
let initial_session = state
.spacetime_client()
.get_match3d_agent_session(session_id.clone(), owner_user_id.clone())
.await
.map_err(|error| {
match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
map_match3d_client_error(error),
)
})?;
let mut config = resolve_config_or_default(initial_session.config.as_ref());
if let Some(generate_click_sound) = generate_click_sound {
config.generate_click_sound = generate_click_sound;
}
// 中文注释:抓大鹅入口已支持表单直创;完整表单创建的 session
// 不需要再伪造三轮聊天,只要配置字段完整即可进入草稿编译。
let has_complete_form_config = !config.theme_text.trim().is_empty()
&& config.clear_count > 0
&& (1..=10).contains(&config.difficulty);
if !has_complete_form_config
&& (initial_session.current_turn < 3 || initial_session.progress_percent < 100)
{
return Err(match3d_bad_request(
request_context,
MATCH3D_AGENT_PROVIDER,
"match3d 创作配置尚未确认完成",
));
}
let requested_game_name = normalize_optional_match3d_text(game_name);
let requested_summary = normalize_optional_match3d_text(summary);
let requested_tags = tags.map(normalize_tags).filter(|items| !items.is_empty());
let requested_cover_image_src = normalize_optional_match3d_text(cover_image_src);
let fallback_work_metadata = fallback_match3d_work_metadata(config.theme_text.as_str());
let profile_id = resolve_match3d_draft_profile_id(&initial_session);
let initial_game_name = requested_game_name
.clone()
.unwrap_or_else(|| fallback_work_metadata.game_name.clone());
let initial_tags = requested_tags
.clone()
.unwrap_or_else(|| fallback_work_metadata.tags.clone());
let billing_asset_id = format!("{}:{}:{}", session_id, profile_id, current_utc_micros());
execute_billable_match3d_draft_generation(
state,
request_context,
owner_user_id.as_str(),
billing_asset_id.as_str(),
async {
let mut session = upsert_match3d_draft_snapshot(
state,
request_context,
authenticated,
session_id.clone(),
owner_user_id.clone(),
profile_id.clone(),
Some(initial_game_name),
requested_summary.clone().or_else(|| Some(String::new())),
Some(serde_json::to_string(&initial_tags).unwrap_or_default()),
requested_cover_image_src.clone(),
None,
None,
)
.await?;
if session.draft.is_none() {
return Err(match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
match3d_bad_gateway("抓大鹅草稿创建失败,请稍后重试"),
));
}
let mut generated_work_metadata = generate_match3d_draft_plan(state, &config).await;
let resolved_game_name = requested_game_name
.unwrap_or_else(|| generated_work_metadata.metadata.game_name.clone());
let resolved_summary = requested_summary
.clone()
.unwrap_or_else(|| generated_work_metadata.metadata.summary.clone());
let resolved_tags = match requested_tags {
Some(tags) => tags,
None => {
generate_match3d_work_tags_for_plan(
state,
resolved_game_name.as_str(),
config.theme_text.as_str(),
resolved_summary.as_str(),
&generated_work_metadata.metadata.tags,
)
.await
}
};
generated_work_metadata.metadata.tags = resolved_tags.clone();
session = upsert_match3d_draft_snapshot(
state,
request_context,
authenticated,
session_id,
owner_user_id.clone(),
profile_id.clone(),
Some(resolved_game_name),
Some(resolved_summary),
Some(serde_json::to_string(&resolved_tags).unwrap_or_default()),
requested_cover_image_src.clone(),
None,
None,
)
.await?;
let existing_assets = get_match3d_existing_generated_item_assets(
state,
owner_user_id.as_str(),
profile_id.as_str(),
)
.await;
let generated_item_assets = generate_match3d_item_assets(
state,
request_context,
authenticated,
owner_user_id.as_str(),
session.session_id.as_str(),
profile_id.as_str(),
&config,
generated_work_metadata.items,
existing_assets,
)
.await?;
let generated_item_assets = ensure_match3d_background_asset(
state,
request_context,
authenticated,
owner_user_id.as_str(),
session.session_id.as_str(),
profile_id.as_str(),
&config,
generated_work_metadata.background_prompt.as_str(),
generated_item_assets,
)
.await?;
let existing_cover_image_src = get_match3d_existing_cover_image_src(
state,
owner_user_id.as_str(),
profile_id.as_str(),
)
.await;
let default_cover_image_src = requested_cover_image_src
.clone()
.or(existing_cover_image_src)
.or_else(|| resolve_match3d_default_cover_image_src(&generated_item_assets));
let next_session = upsert_match3d_draft_snapshot(
state,
request_context,
authenticated,
session.session_id.clone(),
owner_user_id.clone(),
profile_id,
None,
None,
None,
default_cover_image_src,
None,
serialize_match3d_generated_item_assets(&generated_item_assets),
)
.await?;
Ok((next_session, generated_item_assets))
},
)
.await
}
/// 中文注释:抓大鹅草稿生成是一次完整外部生成动作,按 session/profile 幂等预扣 10 泥点。
async fn execute_billable_match3d_draft_generation<T, Fut>(
state: &AppState,
request_context: &RequestContext,
owner_user_id: &str,
billing_asset_id: &str,
operation: Fut,
) -> Result<T, Response>
where
Fut: Future<Output = Result<T, Response>>,
{
let points_consumed = consume_match3d_draft_generation_points(
state,
request_context,
owner_user_id,
billing_asset_id,
)
.await?;
match operation.await {
Ok(value) => Ok(value),
Err(response) => {
if points_consumed {
refund_match3d_draft_generation_points(state, owner_user_id, billing_asset_id)
.await;
}
Err(response)
}
}
}
async fn consume_match3d_draft_generation_points(
state: &AppState,
request_context: &RequestContext,
owner_user_id: &str,
billing_asset_id: &str,
) -> Result<bool, Response> {
let ledger_id = format!(
"asset_operation_consume:{}:match3d_draft_generation:{}",
owner_user_id, billing_asset_id
);
match state
.spacetime_client()
.consume_profile_wallet_points(
owner_user_id.to_string(),
MATCH3D_DRAFT_GENERATION_POINTS_COST,
ledger_id,
current_utc_micros(),
)
.await
{
Ok(_) => Ok(true),
Err(error) if should_skip_asset_operation_billing_for_connectivity(&error) => {
tracing::warn!(
owner_user_id,
billing_asset_id,
error = %error,
"抓大鹅草稿泥点预扣因 SpacetimeDB 连接不可用而降级跳过"
);
Ok(false)
}
Err(error) => Err(match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
map_asset_operation_wallet_error(error),
)),
}
}
async fn refund_match3d_draft_generation_points(
state: &AppState,
owner_user_id: &str,
billing_asset_id: &str,
) {
let ledger_id = format!(
"asset_operation_refund:{}:match3d_draft_generation:{}",
owner_user_id, billing_asset_id
);
if let Err(error) = state
.spacetime_client()
.refund_profile_wallet_points(
owner_user_id.to_string(),
MATCH3D_DRAFT_GENERATION_POINTS_COST,
ledger_id,
current_utc_micros(),
)
.await
{
tracing::error!(
owner_user_id,
billing_asset_id,
error = %error,
"抓大鹅草稿生成失败后的泥点退款失败"
);
}
}
fn resolve_match3d_draft_profile_id(session: &Match3DAgentSessionRecord) -> String {
session
.draft
.as_ref()
.map(|draft| draft.profile_id.trim())
.filter(|profile_id| !profile_id.is_empty())
.or_else(|| {
session
.published_profile_id
.as_deref()
.map(str::trim)
.filter(|profile_id| !profile_id.is_empty())
})
.map(str::to_string)
.unwrap_or_else(|| build_prefixed_uuid_id(MATCH3D_PROFILE_ID_PREFIX))
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn upsert_match3d_draft_snapshot(
state: &AppState,
request_context: &RequestContext,
authenticated: &AuthenticatedAccessToken,
session_id: String,
owner_user_id: String,
profile_id: String,
game_name: Option<String>,
summary_text: Option<String>,
tags_json: Option<String>,
cover_image_src: Option<String>,
cover_asset_id: Option<String>,
generated_item_assets_json: Option<String>,
) -> Result<Match3DAgentSessionRecord, Response> {
state
.spacetime_client()
.compile_match3d_draft(Match3DCompileDraftRecordInput {
session_id,
owner_user_id,
profile_id,
author_display_name: resolve_author_display_name(state, authenticated),
game_name,
summary_text,
tags_json,
cover_image_src,
cover_asset_id,
compiled_at_micros: current_utc_micros(),
generated_item_assets_json,
})
.await
.map_err(|error| {
match3d_error_response(
request_context,
MATCH3D_AGENT_PROVIDER,
map_match3d_client_error(error),
)
})
}
pub(super) fn build_config_from_create_request(
payload: &CreateMatch3DAgentSessionRequest,
) -> Match3DConfigJson {
Match3DConfigJson {
theme_text: payload
.theme_text
.as_deref()
.or(payload.seed_text.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(MATCH3D_DEFAULT_THEME)
.to_string(),
reference_image_src: payload.reference_image_src.clone(),
clear_count: payload.clear_count.unwrap_or(MATCH3D_DEFAULT_CLEAR_COUNT),
difficulty: payload
.difficulty
.unwrap_or(MATCH3D_DEFAULT_DIFFICULTY)
.clamp(1, 10),
asset_style_id: normalize_optional_text(payload.asset_style_id.as_deref()),
asset_style_label: normalize_optional_text(payload.asset_style_label.as_deref()),
asset_style_prompt: normalize_optional_text(payload.asset_style_prompt.as_deref()),
generate_click_sound: payload.generate_click_sound.unwrap_or(false),
}
}
fn build_config_from_message(
session: &Match3DAgentSessionRecord,
payload: &SendMatch3DAgentMessageRequest,
) -> Match3DConfigJson {
let current = resolve_config_or_default(session.config.as_ref());
let text = payload.text.trim();
let reference_image_src = payload
.reference_image_src
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or(current.reference_image_src);
let quick_fill_requested =
payload.quick_fill_requested.unwrap_or(false) || text.contains("自动配置");
let mut theme_text = current.theme_text;
let mut clear_count = current.clear_count.max(1);
let mut difficulty = current.difficulty.clamp(1, 10);
let asset_style_id = current.asset_style_id;
let asset_style_label = current.asset_style_label;
let asset_style_prompt = current.asset_style_prompt;
let generate_click_sound = current.generate_click_sound;
match session.current_turn {
0 => {
theme_text = if quick_fill_requested {
MATCH3D_DEFAULT_THEME.to_string()
} else {
parse_theme_answer(text).unwrap_or(theme_text)
};
}
1 => {
clear_count = if quick_fill_requested {
clear_count
} else {
parse_number_after_keywords(text, &["消除", "次数", "clearCount"])
.unwrap_or(clear_count)
}
.max(1);
}
_ => {
difficulty = if quick_fill_requested {
difficulty
} else {
parse_number_after_keywords(text, &["难度", "difficulty"]).unwrap_or(difficulty)
}
.clamp(1, 10);
}
}
Match3DConfigJson {
theme_text,
reference_image_src,
clear_count,
difficulty,
asset_style_id,
asset_style_label,
asset_style_prompt,
generate_click_sound,
}
}
pub(super) fn resolve_config_or_default(config: Option<&Match3DCreatorConfigRecord>) -> Match3DConfigJson {
config
.map(|config| Match3DConfigJson {
theme_text: config.theme_text.clone(),
reference_image_src: config.reference_image_src.clone(),
clear_count: config.clear_count.max(1),
difficulty: config.difficulty.clamp(1, 10),
asset_style_id: config.asset_style_id.clone(),
asset_style_label: config.asset_style_label.clone(),
asset_style_prompt: config.asset_style_prompt.clone(),
generate_click_sound: config.generate_click_sound,
})
.unwrap_or_else(|| Match3DConfigJson {
theme_text: MATCH3D_DEFAULT_THEME.to_string(),
reference_image_src: None,
clear_count: MATCH3D_DEFAULT_CLEAR_COUNT,
difficulty: MATCH3D_DEFAULT_DIFFICULTY,
asset_style_id: None,
asset_style_label: None,
asset_style_prompt: None,
generate_click_sound: false,
})
}
pub(super) fn normalize_optional_text(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
pub(super) fn serialize_match3d_config(config: &Match3DConfigJson) -> Option<String> {
serde_json::to_string(config).ok()
}
pub(super) fn build_seed_text(
payload: &CreateMatch3DAgentSessionRequest,
config: &Match3DConfigJson,
) -> String {
payload
.seed_text
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.unwrap_or_else(|| {
format!(
"{}题材,消除{}次,难度{}",
config.theme_text, config.clear_count, config.difficulty
)
})
}
fn build_match3d_assistant_reply(config: &Match3DConfigJson) -> String {
format!(
"已确认:{}题材,需要消除 {} 次,共 {} 件物品,难度 {}",
config.theme_text,
config.clear_count,
config.clear_count.saturating_mul(3),
config.difficulty
)
}
pub(super) fn build_match3d_assistant_reply_for_turn(config: &Match3DConfigJson, current_turn: u32) -> String {
match current_turn {
0 => MATCH3D_QUESTION_THEME.to_string(),
1 => MATCH3D_QUESTION_CLEAR_COUNT.to_string(),
2 => MATCH3D_QUESTION_DIFFICULTY.to_string(),
_ => build_match3d_assistant_reply(config),
}
}
pub(super) fn resolve_progress_percent_for_turn(current_turn: u32) -> u32 {
match current_turn {
0 => 0,
1 => 33,
2 => 66,
_ => 100,
}
}
fn parse_theme_answer(text: &str) -> Option<String> {
for marker in ["题材", "主题"] {
if let Some((_, value)) = text.split_once(marker) {
let normalized = value
.trim_matches(|ch: char| ch == ':' || ch == '' || ch.is_whitespace())
.split_whitespace()
.next()
.unwrap_or_default()
.trim_matches(['。', '', ',', ';', ''])
.to_string();
if !normalized.is_empty() {
return Some(normalized);
}
}
}
let trimmed = text.trim();
if (2..=24).contains(&trimmed.chars().count()) && !trimmed.chars().any(|ch| ch.is_ascii_digit())
{
return Some(trimmed.to_string());
}
None
}
fn parse_number_after_keywords(text: &str, keywords: &[&str]) -> Option<u32> {
for keyword in keywords {
if let Some(index) = text.find(keyword) {
let suffix = &text[index + keyword.len()..];
if let Some(value) = first_positive_integer(suffix) {
return Some(value);
}
}
}
first_positive_integer(text)
}
fn first_positive_integer(text: &str) -> Option<u32> {
let mut digits = String::new();
for ch in text.chars() {
if ch.is_ascii_digit() {
digits.push(ch);
} else if !digits.is_empty() {
break;
}
}
digits.parse::<u32>().ok().filter(|value| *value > 0)
}
pub(super) fn normalize_tags(tags: Vec<String>) -> Vec<String> {
let mut result: Vec<String> = Vec::new();
for tag in tags {
let trimmed = normalize_match3d_tag(tag.as_str());
if !trimmed.is_empty() && !result.iter().any(|value| value == &trimmed) {
result.push(trimmed);
}
if result.len() >= 6 {
break;
}
}
result
}
fn normalize_optional_match3d_text(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
async fn generate_match3d_draft_plan(
state: &AppState,
config: &Match3DConfigJson,
) -> Match3DGeneratedDraftPlan {
let Some(llm_client) = state
.creative_agent_gpt5_client()
.or_else(|| state.llm_client())
else {
return fallback_match3d_draft_plan(config);
};
let system_prompt = "你是抓大鹅游戏的草稿生成编辑,只返回 JSON。";
let gameplay_item_count = resolve_match3d_gameplay_item_count(config);
let generated_item_count = resolve_match3d_generated_item_count(config);
let user_prompt = format!(
"题材设定:{}\n请生成抓大鹅游戏草稿生成计划。要求:只返回 JSON 对象,字段为 gameName、summary、tags、backgroundPrompt、items。gameName 为 4 到 12 个中文字符不要包含“作品”“游戏”summary 为 18 到 48 个中文字符的作品描述说明题材氛围和核心体验不要写规则说明tags 为 3 到 6 个中文短标签,每个 2 到 6 个汉字后续会用同一作品信息再次生成作品标签backgroundPrompt 是用于生成局内纯背景图的中文提示词只描述竖屏移动端抓大鹅题材氛围、色彩和环境不得描述锅、圆盘、托盘、拼图槽、物品槽、HUD、UI、文字、按钮、倒计时、分数或物品当前玩法需要 {} 种物品,但素材图固定每 5 个物品一批,因此 items 必须向上补齐并正好返回 {} 项,每项包含 name、itemSize 和 soundPrompt。name 为 2 到 6 个汉字itemSize 只能是“大”“中”“小”之一,按物品真实相对尺寸判断,例如西瓜/大箱子偏大,苹果/杯子偏中,糖果/钥匙偏小soundPrompt 只作为历史字段保留,可返回空字符串。",
config.theme_text, gameplay_item_count, generated_item_count
);
let response = llm_client
.request_text(
LlmTextRequest::new(vec![
LlmMessage::system(system_prompt),
LlmMessage::user(user_prompt),
])
.with_model(MATCH3D_WORK_METADATA_LLM_MODEL)
.with_responses_api(),
)
.await;
match response {
Ok(response) => parse_match3d_draft_plan(response.content.as_str(), config)
.unwrap_or_else(|| fallback_match3d_draft_plan(config)),
Err(error) => {
tracing::warn!(
provider = MATCH3D_AGENT_PROVIDER,
theme_text = config.theme_text.as_str(),
error = %error,
"抓大鹅草稿生成计划失败,降级使用本地生成计划"
);
fallback_match3d_draft_plan(config)
}
}
}
pub(super) fn parse_match3d_draft_plan(
raw: &str,
config: &Match3DConfigJson,
) -> Option<Match3DGeneratedDraftPlan> {
let raw = raw.trim();
let json_text = if let Some(start) = raw.find('{')
&& let Some(end) = raw.rfind('}')
&& end > start
{
&raw[start..=end]
} else {
raw
};
let value = serde_json::from_str::<Value>(json_text).ok()?;
let game_name = normalize_match3d_game_name(value.get("gameName")?.as_str()?);
if game_name.is_empty() {
return None;
}
let tags = value
.get("tags")
.and_then(Value::as_array)
.map(|items| normalize_match3d_tag_candidates(items.iter().filter_map(Value::as_str)))
.unwrap_or_default();
let fallback = fallback_match3d_draft_plan(config);
let summary = value
.get("summary")
.or_else(|| value.get("description"))
.or_else(|| value.get("workSummary"))
.or_else(|| value.get("work_summary"))
.and_then(Value::as_str)
.map(normalize_match3d_work_summary)
.filter(|value| !value.is_empty())
.unwrap_or(fallback.metadata.summary);
let items = value
.get("items")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(|item| {
let name =
normalize_match3d_item_name(item.get("name").and_then(Value::as_str)?);
if name.is_empty() {
return None;
}
let item_size = item
.get("itemSize")
.or_else(|| item.get("item_size"))
.or_else(|| item.get("size"))
.and_then(Value::as_str)
.map(normalize_match3d_item_size)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| infer_match3d_item_size(&name));
let sound_prompt = item
.get("soundPrompt")
.or_else(|| item.get("sound_prompt"))
.and_then(Value::as_str)
.map(normalize_match3d_audio_prompt)
.filter(|value| !value.is_empty())
.unwrap_or_else(|| build_fallback_match3d_item_sound_prompt(config, &name));
Some(Match3DGeneratedItemPlan {
name,
item_size,
sound_prompt,
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
let background_prompt = value
.get("backgroundPrompt")
.or_else(|| value.get("background_prompt"))
.and_then(Value::as_str)
.map(normalize_match3d_background_prompt)
.filter(|value| !value.is_empty())
.unwrap_or(fallback.background_prompt);
Some(Match3DGeneratedDraftPlan {
metadata: Match3DGeneratedWorkMetadata {
game_name,
summary,
tags: normalize_match3d_tag_candidates(tags),
},
items: normalize_match3d_item_plan(config, items),
background_prompt,
})
}
#[cfg(test)]
pub(super) fn parse_match3d_work_metadata(raw: &str) -> Option<Match3DGeneratedWorkMetadata> {
let config = Match3DConfigJson {
theme_text: MATCH3D_DEFAULT_THEME.to_string(),
reference_image_src: None,
clear_count: MATCH3D_DEFAULT_CLEAR_COUNT,
difficulty: MATCH3D_DEFAULT_DIFFICULTY,
asset_style_id: None,
asset_style_label: None,
asset_style_prompt: None,
generate_click_sound: false,
};
parse_match3d_draft_plan(raw, &config).map(|plan| plan.metadata)
}
fn normalize_match3d_game_name(raw: &str) -> String {
raw.trim()
.trim_matches(['"', '\'', '“', '”', '。', '', ',', '、'])
.chars()
.filter(|character| !character.is_control())
.take(16)
.collect::<String>()
.trim()
.to_string()
}
fn normalize_match3d_work_summary(raw: &str) -> String {
raw.trim()
.trim_matches(['"', '\'', '“', '”'])
.split_whitespace()
.collect::<Vec<_>>()
.join("")
.chars()
.filter(|character| !character.is_control())
.take(80)
.collect::<String>()
.trim()
.to_string()
}
pub(super) fn fallback_match3d_work_metadata(theme_text: &str) -> Match3DGeneratedWorkMetadata {
let theme = theme_text.trim();
let normalized_theme = if theme.is_empty() { "主题" } else { theme };
Match3DGeneratedWorkMetadata {
game_name: format!("{normalized_theme}抓大鹅"),
summary: normalize_match3d_work_summary(
format!("{normalized_theme}主题的轻量抓取消除作品,适合快速体验。").as_str(),
),
tags: normalize_match3d_tag_candidates([normalized_theme, "抓大鹅", "经典消除", "2D素材"]),
}
}
fn fallback_match3d_draft_plan(config: &Match3DConfigJson) -> Match3DGeneratedDraftPlan {
let metadata = fallback_match3d_work_metadata(config.theme_text.as_str());
let items = fallback_match3d_item_names(config.theme_text.as_str())
.into_iter()
.take(resolve_match3d_generated_item_count(config))
.map(|name| Match3DGeneratedItemPlan {
item_size: infer_match3d_item_size(&name),
sound_prompt: build_fallback_match3d_item_sound_prompt(config, &name),
name,
})
.collect::<Vec<_>>();
Match3DGeneratedDraftPlan {
background_prompt: build_fallback_match3d_background_prompt(config),
metadata,
items,
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
pub(super) fn normalize_match3d_run_status(value: &str) -> &str {
match value {
"Running" => "running",
"Won" => "won",
"Failed" => "failed",
"Stopped" => "stopped",
_ => value,
}
}
pub(super) fn normalize_match3d_item_state(value: &str) -> &str {
match value {
"InBoard" => "in_board",
"InTray" => "in_tray",
"Cleared" => "cleared",
_ => value,
}
}
pub(super) fn normalize_match3d_failure_reason(value: &str) -> &str {
match value {
"TimeUp" => "time_up",
"TrayFull" => "tray_full",
_ => value,
}
}
pub(super) fn normalize_match3d_click_reject_reason(value: &str) -> &str {
match value {
"RejectedNotClickable" => "item_not_clickable",
"RejectedAlreadyMoved" => "item_not_in_board",
"RejectedTrayFull" => "tray_full",
"VersionConflict" => "snapshot_version_mismatch",
"RunFinished" => "run_not_active",
_ => value,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,483 @@
use super::*;
pub(super) async fn generate_match3d_material_sheet(
state: &AppState,
config: &Match3DConfigJson,
item_names: &[String],
) -> Result<Match3DMaterialSheet, AppError> {
let settings = require_match3d_vector_engine_gemini_image_settings(state)?;
let http_client = build_match3d_vector_engine_gemini_image_http_client(&settings)?;
let prompt = build_match3d_material_sheet_prompt(config, item_names);
let negative_prompt = build_match3d_material_sheet_negative_prompt(config);
let generated = create_match3d_vector_engine_gemini_image_generation(
&http_client,
&settings,
prompt.as_str(),
negative_prompt.as_str(),
"抓大鹅素材图生成失败",
)
.await?;
let image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": "抓大鹅素材图生成失败:未返回图片",
}))
})?;
Ok(Match3DMaterialSheet {
task_id: generated.task_id,
image,
})
}
fn require_match3d_vector_engine_gemini_image_settings(
state: &AppState,
) -> Result<Match3DVectorEngineGeminiImageSettings, AppError> {
let base_url = state
.config
.vector_engine_base_url
.trim()
.trim_end_matches('/');
if base_url.is_empty() {
return Err(
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "vector-engine-gemini",
"reason": "VECTOR_ENGINE_BASE_URL 未配置",
})),
);
}
let api_key = state
.config
.vector_engine_api_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
"provider": "vector-engine-gemini",
"reason": "VECTOR_ENGINE_API_KEY 未配置",
}))
})?;
Ok(Match3DVectorEngineGeminiImageSettings {
base_url: base_url.to_string(),
api_key: api_key.to_string(),
request_timeout_ms: state.config.vector_engine_image_request_timeout_ms.max(1),
})
}
fn build_match3d_vector_engine_gemini_image_http_client(
settings: &Match3DVectorEngineGeminiImageSettings,
) -> Result<reqwest::Client, AppError> {
reqwest::Client::builder()
.timeout(Duration::from_millis(settings.request_timeout_ms))
.build()
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "vector-engine-gemini",
"message": format!("构造抓大鹅 VectorEngine Gemini 图片生成 HTTP 客户端失败:{error}"),
}))
})
}
async fn create_match3d_vector_engine_gemini_image_generation(
http_client: &reqwest::Client,
settings: &Match3DVectorEngineGeminiImageSettings,
prompt: &str,
negative_prompt: &str,
failure_context: &str,
) -> Result<OpenAiGeneratedImages, AppError> {
let request_body = build_match3d_vector_engine_gemini_image_request_body(
prompt,
negative_prompt,
MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_ASPECT_RATIO,
);
let response = http_client
.post(build_match3d_vector_engine_gemini_generate_content_url(
settings,
))
.query(&[("key", settings.api_key.as_str())])
.header(header::ACCEPT, "application/json")
.header(header::CONTENT_TYPE, "application/json")
.json(&request_body)
.send()
.await
.map_err(|error| {
map_match3d_vector_engine_gemini_image_request_error(format!(
"{failure_context}:调用 VectorEngine Gemini 图片生成失败:{error}"
))
})?;
let status = response.status();
let response_text = response.text().await.map_err(|error| {
map_match3d_vector_engine_gemini_image_request_error(format!(
"{failure_context}:读取 VectorEngine Gemini 图片生成响应失败:{error}"
))
})?;
if !status.is_success() {
return Err(map_match3d_vector_engine_gemini_image_upstream_error(
status,
response_text.as_str(),
failure_context,
));
}
let payload = parse_match3d_json_payload(
response_text.as_str(),
"解析抓大鹅 VectorEngine Gemini 图片生成响应失败",
"vector-engine-gemini",
)?;
let image_urls = extract_match3d_image_urls(&payload);
if !image_urls.is_empty() {
return download_match3d_images_from_urls(
http_client,
format!("vector-engine-gemini-{}", current_utc_micros()),
image_urls,
1,
"vector-engine-gemini",
)
.await;
}
let b64_images = extract_match3d_b64_images(&payload);
if !b64_images.is_empty() {
return Ok(match3d_images_from_base64(
format!("vector-engine-gemini-{}", current_utc_micros()),
b64_images,
1,
));
}
Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": "抓大鹅 VectorEngine Gemini 图片生成未返回图片",
"rawExcerpt": trim_match3d_upstream_excerpt(response_text.as_str(), 800),
})),
)
}
pub(super) fn build_match3d_vector_engine_gemini_image_request_body(
prompt: &str,
negative_prompt: &str,
aspect_ratio: &str,
) -> Value {
json!({
"contents": [{
"role": "user",
"parts": [{
"text": build_match3d_vector_engine_gemini_prompt(prompt, negative_prompt),
}],
}],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": aspect_ratio,
},
},
})
}
pub(super) fn build_match3d_vector_engine_gemini_generate_content_url(
settings: &Match3DVectorEngineGeminiImageSettings,
) -> String {
let base_url = settings.base_url.trim_end_matches("/v1");
format!(
"{}/v1beta/models/{}:generateContent",
base_url, MATCH3D_MATERIAL_VECTOR_ENGINE_GEMINI_MODEL
)
}
fn build_match3d_vector_engine_gemini_prompt(prompt: &str, negative_prompt: &str) -> String {
let prompt = prompt.trim();
let negative_prompt = negative_prompt.trim();
if negative_prompt.is_empty() {
return prompt.to_string();
}
format!("{prompt}\n避免:{negative_prompt}")
}
async fn download_match3d_images_from_urls(
http_client: &reqwest::Client,
task_id: String,
image_urls: Vec<String>,
candidate_count: u32,
provider: &str,
) -> Result<OpenAiGeneratedImages, AppError> {
let mut images = Vec::with_capacity(candidate_count.clamp(1, 4) as usize);
for image_url in image_urls
.into_iter()
.take(candidate_count.clamp(1, 4) as usize)
{
images
.push(download_match3d_remote_image(http_client, image_url.as_str(), provider).await?);
}
Ok(OpenAiGeneratedImages {
task_id,
actual_prompt: None,
images,
})
}
async fn download_match3d_remote_image(
http_client: &reqwest::Client,
image_url: &str,
provider: &str,
) -> Result<DownloadedOpenAiImage, AppError> {
let response = http_client.get(image_url).send().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("下载抓大鹅生成图片失败:{error}"),
}))
})?;
let status = response.status();
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("image/png")
.to_string();
let body = response.bytes().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("读取抓大鹅生成图片内容失败:{error}"),
}))
})?;
if !status.is_success() {
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": "下载抓大鹅生成图片失败",
"status": status.as_u16(),
})),
);
}
let mime_type = normalize_match3d_downloaded_image_mime_type(content_type.as_str());
Ok(DownloadedOpenAiImage {
extension: match3d_mime_to_extension(mime_type.as_str()).to_string(),
mime_type,
bytes: body.to_vec(),
})
}
fn match3d_images_from_base64(
task_id: String,
b64_images: Vec<String>,
candidate_count: u32,
) -> OpenAiGeneratedImages {
let images = b64_images
.into_iter()
.take(candidate_count.clamp(1, 4) as usize)
.filter_map(|raw| decode_match3d_base64_image(raw.as_str()))
.collect();
OpenAiGeneratedImages {
task_id,
actual_prompt: None,
images,
}
}
fn decode_match3d_base64_image(raw: &str) -> Option<DownloadedOpenAiImage> {
let bytes = BASE64_STANDARD.decode(raw.trim()).ok()?;
let mime_type = infer_match3d_image_mime_type(bytes.as_slice()).to_string();
Some(DownloadedOpenAiImage {
extension: match3d_mime_to_extension(mime_type.as_str()).to_string(),
mime_type,
bytes,
})
}
fn parse_match3d_json_payload(
raw_text: &str,
failure_context: &str,
provider: &str,
) -> Result<Value, AppError> {
serde_json::from_str::<Value>(raw_text).map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": provider,
"message": format!("{failure_context}{error}"),
"rawExcerpt": trim_match3d_upstream_excerpt(raw_text, 800),
}))
})
}
fn extract_match3d_image_urls(payload: &Value) -> Vec<String> {
let mut urls = Vec::new();
collect_match3d_strings_by_key(payload, "url", &mut urls);
collect_match3d_strings_by_key(payload, "image", &mut urls);
collect_match3d_strings_by_key(payload, "image_url", &mut urls);
let mut deduped = Vec::new();
for url in urls {
if (url.starts_with("http://") || url.starts_with("https://")) && !deduped.contains(&url) {
deduped.push(url);
}
}
deduped
}
pub(super) fn extract_match3d_b64_images(payload: &Value) -> Vec<String> {
let mut values = Vec::new();
collect_match3d_strings_by_key(payload, "b64_json", &mut values);
collect_match3d_inline_image_data(payload, &mut values);
values
}
fn collect_match3d_inline_image_data(payload: &Value, results: &mut Vec<String>) {
match payload {
Value::Array(entries) => {
for entry in entries {
collect_match3d_inline_image_data(entry, results);
}
}
Value::Object(object) => {
for key in ["inlineData", "inline_data"] {
if let Some(Value::Object(inline_data)) = object.get(key) {
let mime_type = inline_data
.get("mimeType")
.or_else(|| inline_data.get("mime_type"))
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or("image/png")
.to_ascii_lowercase();
if !mime_type.is_empty() && !mime_type.starts_with("image/") {
continue;
}
if let Some(data) = inline_data
.get("data")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
results.push(data.to_string());
}
}
}
for nested_value in object.values() {
collect_match3d_inline_image_data(nested_value, results);
}
}
_ => {}
}
}
fn find_first_match3d_string_by_key(payload: &Value, target_key: &str) -> Option<String> {
let mut results = Vec::new();
collect_match3d_strings_by_key(payload, target_key, &mut results);
results.into_iter().next()
}
fn collect_match3d_strings_by_key(payload: &Value, target_key: &str, results: &mut Vec<String>) {
match payload {
Value::Array(entries) => {
for entry in entries {
collect_match3d_strings_by_key(entry, target_key, results);
}
}
Value::Object(object) => {
for (key, nested_value) in object {
if key == target_key {
match nested_value {
Value::String(text) => {
let text = text.trim();
if !text.is_empty() {
results.push(text.to_string());
}
}
Value::Array(entries) => {
for entry in entries {
if let Some(text) = entry
.as_str()
.map(str::trim)
.filter(|value| !value.is_empty())
{
results.push(text.to_string());
}
}
}
_ => {}
}
}
collect_match3d_strings_by_key(nested_value, target_key, results);
}
}
_ => {}
}
}
fn map_match3d_vector_engine_gemini_image_request_error(message: String) -> AppError {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"message": message,
}))
}
fn map_match3d_vector_engine_gemini_image_upstream_error(
upstream_status: reqwest::StatusCode,
raw_text: &str,
fallback_message: &str,
) -> AppError {
let message = parse_match3d_api_error_message(raw_text, fallback_message);
let raw_excerpt = trim_match3d_upstream_excerpt(raw_text, 800);
tracing::warn!(
provider = "vector-engine-gemini",
upstream_status = upstream_status.as_u16(),
message = %message,
raw_excerpt = %raw_excerpt,
"抓大鹅 VectorEngine Gemini 图片生成上游请求失败"
);
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine-gemini",
"upstreamStatus": upstream_status.as_u16(),
"message": message,
"rawExcerpt": raw_excerpt,
}))
}
fn parse_match3d_api_error_message(raw_text: &str, fallback_message: &str) -> String {
let trimmed = raw_text.trim();
if trimmed.is_empty() {
return fallback_message.to_string();
}
if let Ok(payload) = serde_json::from_str::<Value>(trimmed) {
for key in ["message", "code"] {
if let Some(value) = find_first_match3d_string_by_key(&payload, key) {
return if key == "message" {
value
} else {
format!("{fallback_message}{value}")
};
}
}
}
trimmed.to_string()
}
fn trim_match3d_upstream_excerpt(raw_text: &str, max_chars: usize) -> String {
raw_text.chars().take(max_chars).collect()
}
fn normalize_match3d_downloaded_image_mime_type(content_type: &str) -> String {
let mime_type = content_type
.split(';')
.next()
.map(str::trim)
.unwrap_or("image/png");
match mime_type {
"image/png" | "image/webp" | "image/jpeg" | "image/jpg" | "image/gif" => {
mime_type.to_string()
}
_ => "image/png".to_string(),
}
}
pub(super) fn match3d_mime_to_extension(mime_type: &str) -> &str {
match mime_type {
"image/png" => "png",
"image/webp" => "webp",
"image/gif" => "gif",
"image/jpeg" | "image/jpg" => "jpg",
_ => "png",
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
use super::*;
use crate::mapper::{
custom_world::{format_ai_result_reference_kind, format_ai_task_kind},
inventory::map_ai_result_reference_kind,
npc::map_ai_task_kind,
};
impl From<DomainAiTaskCreateInput> for AiTaskCreateInput {
fn from(input: DomainAiTaskCreateInput) -> Self {
Self {
task_id: input.task_id,
task_kind: map_ai_task_kind(input.task_kind),
owner_user_id: input.owner_user_id,
request_label: input.request_label,
source_module: input.source_module,
source_entity_id: input.source_entity_id,
request_payload_json: input.request_payload_json,
stages: input.stages.into_iter().map(Into::into).collect(),
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiTaskStartInput> for AiTaskStartInput {
fn from(input: DomainAiTaskStartInput) -> Self {
Self {
task_id: input.task_id,
started_at_micros: input.started_at_micros,
}
}
}
impl From<DomainAiTaskStageStartInput> for AiTaskStageStartInput {
fn from(input: DomainAiTaskStageStartInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
started_at_micros: input.started_at_micros,
}
}
}
impl From<DomainAiTextChunkAppendInput> for AiTextChunkAppendInput {
fn from(input: DomainAiTextChunkAppendInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
sequence: input.sequence,
delta_text: input.delta_text,
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiStageCompletionInput> for AiStageCompletionInput {
fn from(input: DomainAiStageCompletionInput) -> Self {
Self {
task_id: input.task_id,
stage_kind: map_ai_task_stage_kind(input.stage_kind),
text_output: input.text_output,
structured_payload_json: input.structured_payload_json,
warning_messages: input.warning_messages,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiResultReferenceInput> for AiResultReferenceInput {
fn from(input: DomainAiResultReferenceInput) -> Self {
Self {
task_id: input.task_id,
reference_kind: map_ai_result_reference_kind(input.reference_kind),
reference_id: input.reference_id,
label: input.label,
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainAiTaskFinishInput> for AiTaskFinishInput {
fn from(input: DomainAiTaskFinishInput) -> Self {
Self {
task_id: input.task_id,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskFailureInput> for AiTaskFailureInput {
fn from(input: DomainAiTaskFailureInput) -> Self {
Self {
task_id: input.task_id,
failure_message: input.failure_message,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskCancelInput> for AiTaskCancelInput {
fn from(input: DomainAiTaskCancelInput) -> Self {
Self {
task_id: input.task_id,
completed_at_micros: input.completed_at_micros,
}
}
}
impl From<DomainAiTaskStageBlueprint> for AiTaskStageBlueprint {
fn from(blueprint: DomainAiTaskStageBlueprint) -> Self {
Self {
stage_kind: map_ai_task_stage_kind(blueprint.stage_kind),
label: blueprint.label,
detail: blueprint.detail,
order: blueprint.order,
}
}
}
pub(crate) fn map_ai_task_procedure_result(
result: AiTaskProcedureResult,
) -> Result<AiTaskMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let task = result
.task
.ok_or_else(|| SpacetimeClientError::missing_snapshot("ai_task 快照"))?;
Ok(AiTaskMutationRecord {
task: map_ai_task_snapshot(task),
text_chunk: result.text_chunk.map(map_ai_text_chunk_snapshot),
})
}
pub(crate) fn map_ai_task_snapshot(snapshot: AiTaskSnapshot) -> AiTaskRecord {
AiTaskRecord {
task_id: snapshot.task_id,
task_kind: format_ai_task_kind(snapshot.task_kind).to_string(),
owner_user_id: snapshot.owner_user_id,
request_label: snapshot.request_label,
source_module: snapshot.source_module,
source_entity_id: snapshot.source_entity_id,
request_payload_json: snapshot.request_payload_json,
status: format_ai_task_status(snapshot.status).to_string(),
failure_message: snapshot.failure_message,
stages: snapshot
.stages
.into_iter()
.map(map_ai_task_stage_snapshot)
.collect(),
result_references: snapshot
.result_references
.into_iter()
.map(map_ai_result_reference_snapshot)
.collect(),
latest_text_output: snapshot.latest_text_output,
latest_structured_payload_json: snapshot.latest_structured_payload_json,
version: snapshot.version,
created_at: format_timestamp_micros(snapshot.created_at_micros),
started_at: snapshot.started_at_micros.map(format_timestamp_micros),
completed_at: snapshot.completed_at_micros.map(format_timestamp_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn map_ai_task_stage_snapshot(snapshot: AiTaskStageSnapshot) -> AiTaskStageRecord {
AiTaskStageRecord {
stage_kind: format_ai_task_stage_kind(snapshot.stage_kind).to_string(),
label: snapshot.label,
detail: snapshot.detail,
order: snapshot.order,
status: format_ai_task_stage_status(snapshot.status).to_string(),
text_output: snapshot.text_output,
structured_payload_json: snapshot.structured_payload_json,
warning_messages: snapshot.warning_messages,
started_at: snapshot.started_at_micros.map(format_timestamp_micros),
completed_at: snapshot.completed_at_micros.map(format_timestamp_micros),
}
}
pub(crate) fn map_ai_text_chunk_snapshot(snapshot: AiTextChunkSnapshot) -> AiTextChunkRecord {
AiTextChunkRecord {
chunk_id: snapshot.chunk_id,
task_id: snapshot.task_id,
stage_kind: format_ai_task_stage_kind(snapshot.stage_kind).to_string(),
sequence: snapshot.sequence,
delta_text: snapshot.delta_text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_ai_result_reference_snapshot(
snapshot: AiResultReferenceSnapshot,
) -> AiResultReferenceRecord {
AiResultReferenceRecord {
result_ref_id: snapshot.result_ref_id,
task_id: snapshot.task_id,
reference_kind: format_ai_result_reference_kind(snapshot.reference_kind).to_string(),
reference_id: snapshot.reference_id,
label: snapshot.label,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_ai_task_stage_kind(value: DomainAiTaskStageKind) -> AiTaskStageKind {
match value {
DomainAiTaskStageKind::PreparePrompt => AiTaskStageKind::PreparePrompt,
DomainAiTaskStageKind::RequestModel => AiTaskStageKind::RequestModel,
DomainAiTaskStageKind::RepairResponse => AiTaskStageKind::RepairResponse,
DomainAiTaskStageKind::NormalizeResult => AiTaskStageKind::NormalizeResult,
DomainAiTaskStageKind::PersistResult => AiTaskStageKind::PersistResult,
}
}
pub(crate) fn format_ai_task_status(value: AiTaskStatus) -> &'static str {
match value {
AiTaskStatus::Pending => "pending",
AiTaskStatus::Running => "running",
AiTaskStatus::Completed => "completed",
AiTaskStatus::Failed => "failed",
AiTaskStatus::Cancelled => "cancelled",
}
}
pub(crate) fn format_ai_task_stage_kind(value: AiTaskStageKind) -> &'static str {
match value {
AiTaskStageKind::PreparePrompt => "prepare_prompt",
AiTaskStageKind::RequestModel => "request_model",
AiTaskStageKind::RepairResponse => "repair_response",
AiTaskStageKind::NormalizeResult => "normalize_result",
AiTaskStageKind::PersistResult => "persist_result",
}
}
pub(crate) fn format_ai_task_stage_status(value: AiTaskStageStatus) -> &'static str {
match value {
AiTaskStageStatus::Pending => "pending",
AiTaskStageStatus::Running => "running",
AiTaskStageStatus::Completed => "completed",
AiTaskStageStatus::Skipped => "skipped",
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskStageRecord {
pub stage_kind: String,
pub label: String,
pub detail: String,
pub order: u32,
pub status: String,
pub text_output: Option<String>,
pub structured_payload_json: Option<String>,
pub warning_messages: Vec<String>,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiResultReferenceRecord {
pub result_ref_id: String,
pub task_id: String,
pub reference_kind: String,
pub reference_id: String,
pub label: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTextChunkRecord {
pub chunk_id: String,
pub task_id: String,
pub stage_kind: String,
pub sequence: u32,
pub delta_text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskRecord {
pub task_id: String,
pub task_kind: String,
pub owner_user_id: String,
pub request_label: String,
pub source_module: String,
pub source_entity_id: Option<String>,
pub request_payload_json: Option<String>,
pub status: String,
pub failure_message: Option<String>,
pub stages: Vec<AiTaskStageRecord>,
pub result_references: Vec<AiResultReferenceRecord>,
pub latest_text_output: Option<String>,
pub latest_structured_payload_json: Option<String>,
pub version: u32,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AiTaskMutationRecord {
pub task: AiTaskRecord,
pub text_chunk: Option<AiTextChunkRecord>,
}

View File

@@ -0,0 +1,382 @@
use super::*;
impl From<module_assets::AssetEntityBindingInput> for AssetEntityBindingInput {
fn from(input: module_assets::AssetEntityBindingInput) -> Self {
Self {
binding_id: input.binding_id,
asset_object_id: input.asset_object_id,
entity_kind: input.entity_kind,
entity_id: input.entity_id,
slot: input.slot,
asset_kind: input.asset_kind,
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_assets::AssetObjectUpsertInput> for AssetObjectUpsertInput {
fn from(input: module_assets::AssetObjectUpsertInput) -> Self {
Self {
asset_object_id: input.asset_object_id,
bucket: input.bucket,
object_key: input.object_key,
access_policy: map_access_policy(input.access_policy),
content_type: input.content_type,
content_length: input.content_length,
content_hash: input.content_hash,
version: input.version,
source_job_id: input.source_job_id,
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
entity_id: input.entity_id,
asset_kind: input.asset_kind,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_assets::AssetHistoryListInput> for AssetHistoryListInput {
fn from(input: module_assets::AssetHistoryListInput) -> Self {
Self {
asset_kind: input.asset_kind,
limit: input.limit,
}
}
}
pub(crate) fn map_procedure_result(
result: AssetObjectProcedureResult,
) -> Result<AssetObjectRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("对象快照"))?;
Ok(build_asset_object_record(map_snapshot(snapshot)))
}
pub(crate) fn map_entity_binding_procedure_result(
result: AssetEntityBindingProcedureResult,
) -> Result<AssetEntityBindingRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("绑定快照"))?;
Ok(build_asset_entity_binding_record(
map_entity_binding_snapshot(snapshot),
))
}
pub(crate) fn map_entity_binding_snapshot(
snapshot: AssetEntityBindingSnapshot,
) -> module_assets::AssetEntityBindingSnapshot {
module_assets::AssetEntityBindingSnapshot {
binding_id: snapshot.binding_id,
asset_object_id: snapshot.asset_object_id,
entity_kind: snapshot.entity_kind,
entity_id: snapshot.entity_id,
slot: snapshot.slot,
asset_kind: snapshot.asset_kind,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_snapshot(
snapshot: AssetObjectUpsertSnapshot,
) -> module_assets::AssetObjectUpsertSnapshot {
module_assets::AssetObjectUpsertSnapshot {
asset_object_id: snapshot.asset_object_id,
bucket: snapshot.bucket,
object_key: snapshot.object_key,
access_policy: map_access_policy_back(snapshot.access_policy),
content_type: snapshot.content_type,
content_length: snapshot.content_length,
content_hash: snapshot.content_hash,
version: snapshot.version,
source_job_id: snapshot.source_job_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
entity_id: snapshot.entity_id,
asset_kind: snapshot.asset_kind,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_access_policy(
value: AssetObjectAccessPolicy,
) -> crate::module_bindings::AssetObjectAccessPolicy {
match value {
AssetObjectAccessPolicy::Private => {
crate::module_bindings::AssetObjectAccessPolicy::Private
}
AssetObjectAccessPolicy::PublicRead => {
crate::module_bindings::AssetObjectAccessPolicy::PublicRead
}
}
}
pub(crate) fn map_access_policy_back(
value: crate::module_bindings::AssetObjectAccessPolicy,
) -> AssetObjectAccessPolicy {
match value {
crate::module_bindings::AssetObjectAccessPolicy::Private => {
AssetObjectAccessPolicy::Private
}
crate::module_bindings::AssetObjectAccessPolicy::PublicRead => {
AssetObjectAccessPolicy::PublicRead
}
}
}
impl TryFrom<&str> for BigFishAssetKind {
type Error = SpacetimeClientError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.trim() {
"level_main_image" => Ok(Self::LevelMainImage),
"level_motion" => Ok(Self::LevelMotion),
"stage_background" => Ok(Self::StageBackground),
other => Err(SpacetimeClientError::Runtime(format!(
"big fish asset kind `{other}` 当前尚未支持"
))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldDraftCardRecord {
pub card_id: String,
pub kind: String,
pub title: String,
pub subtitle: String,
pub summary: String,
pub status: String,
pub linked_ids: Vec<String>,
pub warning_count: u32,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
pub detail_payload: Option<serde_json::Value>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldDraftCardDetailRecord {
pub card_id: String,
pub kind: String,
pub title: String,
pub sections: Vec<CustomWorldDraftCardDetailSectionRecord>,
pub linked_ids: Vec<String>,
pub locked: bool,
pub editable: bool,
pub editable_section_ids: Vec<String>,
pub warning_messages: Vec<String>,
pub asset_status: Option<String>,
pub asset_status_label: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldAgentSessionRecord {
pub session_id: String,
pub seed_text: String,
pub current_turn: u32,
pub anchor_content: serde_json::Value,
pub progress_percent: u32,
pub last_assistant_reply: Option<String>,
pub stage: String,
pub focus_card_id: Option<String>,
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<CustomWorldAgentMessageRecord>,
pub draft_cards: Vec<CustomWorldDraftCardRecord>,
pub pending_clarifications: Vec<serde_json::Value>,
pub suggested_actions: Vec<serde_json::Value>,
pub recommended_replies: Vec<String>,
pub quality_findings: Vec<serde_json::Value>,
pub asset_coverage: serde_json::Value,
pub checkpoints: Vec<CustomWorldCheckpointRecord>,
pub supported_actions: Vec<CustomWorldSupportedActionRecord>,
pub publish_gate: Option<CustomWorldPublishGateRecord>,
pub result_preview: Option<serde_json::Value>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub lock_state_json: Option<String>,
pub draft_profile_json: Option<String>,
pub pending_clarifications_json: String,
pub suggested_actions_json: String,
pub recommended_replies_json: String,
pub quality_findings_json: String,
pub asset_coverage_json: String,
pub checkpoints_json: String,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub operation_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub phase_label: String,
pub phase_detail: String,
pub operation_status: String,
pub operation_progress: u32,
pub stage: String,
pub progress_percent: u32,
pub focus_card_id: Option<String>,
pub anchor_content_json: String,
pub creator_intent_json: Option<String>,
pub creator_intent_readiness_json: String,
pub anchor_pack_json: Option<String>,
pub draft_profile_json: Option<String>,
pub pending_clarifications_json: String,
pub suggested_actions_json: String,
pub recommended_replies_json: String,
pub quality_findings_json: String,
pub asset_coverage_json: String,
pub error_message: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub seed_text: String,
pub source_asset_ids_json: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub draft_json: Option<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelWorkUpdateRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub work_title: String,
pub work_description: String,
pub tags_json: String,
pub cover_image_src: Option<String>,
pub source_asset_ids_json: String,
pub draft_json: String,
pub publish_ready: bool,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelAgentSessionRecord {
pub session_id: String,
pub owner_user_id: String,
pub source_mode: String,
pub status: String,
pub seed_text: String,
pub source_asset_ids: Vec<String>,
pub current_turn: u32,
pub progress_percent: u32,
pub messages: Vec<VisualNovelAgentMessageRecord>,
pub draft: Option<serde_json::Value>,
pub pending_action: Option<serde_json::Value>,
pub last_assistant_reply: Option<String>,
pub published_profile_id: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelWorkProfileRecord {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub source_asset_ids: Vec<String>,
pub draft: serde_json::Value,
pub publication_status: String,
pub publish_ready: bool,
pub play_count: u32,
pub created_at: String,
pub updated_at: String,
pub published_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetGenerateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub asset_kind: String,
pub level: Option<u32>,
pub motion_key: Option<String>,
pub asset_url: Option<String>,
pub generated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetSlotRecord {
pub slot_id: String,
pub asset_kind: String,
pub level: Option<u32>,
pub motion_key: Option<String>,
pub status: String,
pub asset_url: Option<String>,
pub prompt_snapshot: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAssetCoverageRecord {
pub level_main_image_ready_count: u32,
pub level_motion_ready_count: u32,
pub background_ready: bool,
pub required_level_count: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishSessionRecord {
pub session_id: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
pub anchor_pack: BigFishAnchorPackRecord,
pub draft: Option<BigFishGameDraftRecord>,
pub asset_slots: Vec<BigFishAssetSlotRecord>,
pub asset_coverage: BigFishAssetCoverageRecord,
pub messages: Vec<BigFishAgentMessageRecord>,
pub last_assistant_reply: Option<String>,
pub publish_ready: bool,
pub updated_at: String,
}

View File

@@ -0,0 +1,42 @@
use super::*;
pub(crate) fn map_auth_store_snapshot_procedure_result(
result: AuthStoreSnapshotProcedureResult,
) -> Result<AuthStoreSnapshotRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let record = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("认证快照"))?;
Ok(map_auth_store_snapshot_record(record))
}
pub(crate) fn map_auth_store_snapshot_record(
record: crate::module_bindings::AuthStoreSnapshotRecord,
) -> crate::AuthStoreSnapshotRecord {
crate::AuthStoreSnapshotRecord {
snapshot_json: record.snapshot_json,
updated_at_micros: record.updated_at_micros,
}
}
pub(crate) fn map_auth_store_snapshot_import_procedure_result(
result: AuthStoreSnapshotImportProcedureResult,
) -> Result<AuthStoreSnapshotImportRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let record = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("认证快照导入结果"))?;
Ok(AuthStoreSnapshotImportRecord {
imported_user_count: record.imported_user_count,
imported_identity_count: record.imported_identity_count,
imported_refresh_session_count: record.imported_refresh_session_count,
})
}

View File

@@ -0,0 +1,94 @@
use super::*;
pub(crate) fn map_bark_battle_draft_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.draft_config
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle draft config"))
.map(bark_battle_draft_config_to_value)
}
pub(crate) fn map_bark_battle_runtime_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRuntimeConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.runtime_config
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle runtime config"))
.map(bark_battle_runtime_config_to_value)
}
pub(crate) fn map_bark_battle_run_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle run"))
.map(bark_battle_run_to_value)
}
fn bark_battle_draft_config_to_value(snapshot: BarkBattleDraftConfigSnapshot) -> serde_json::Value {
serde_json::json!({
"draftId": snapshot.draft_id,
"ownerUserId": snapshot.owner_user_id,
"workId": snapshot.work_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"configJson": snapshot.config_json,
"editorStateJson": snapshot.editor_state_json,
"createdAtMicros": snapshot.created_at_micros,
"updatedAtMicros": snapshot.updated_at_micros,
})
}
fn bark_battle_runtime_config_to_value(
snapshot: BarkBattleRuntimeConfigSnapshot,
) -> serde_json::Value {
serde_json::json!({
"workId": snapshot.work_id,
"ownerUserId": snapshot.owner_user_id,
"sourceDraftId": snapshot.source_draft_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"configJson": snapshot.config_json,
"publishedSnapshotJson": snapshot.published_snapshot_json,
"publishedAtMicros": snapshot.published_at_micros,
"updatedAtMicros": snapshot.updated_at_micros,
})
}
fn bark_battle_run_to_value(snapshot: BarkBattleRunSnapshot) -> serde_json::Value {
serde_json::json!({
"runId": snapshot.run_id,
"ownerUserId": snapshot.owner_user_id,
"workId": snapshot.work_id,
"configVersion": snapshot.config_version,
"rulesetVersion": snapshot.ruleset_version,
"difficultyPreset": snapshot.difficulty_preset,
"leaderboardEnabled": snapshot.leaderboard_enabled,
"status": snapshot.status,
"clientStartedAtMicros": snapshot.client_started_at_micros,
"serverStartedAtMicros": snapshot.server_started_at_micros,
"clientFinishedAtMicros": snapshot.client_finished_at_micros,
"serverFinishedAtMicros": snapshot.server_finished_at_micros,
"metricsJson": snapshot.metrics_json,
"serverResult": snapshot.server_result,
"validationStatus": snapshot.validation_status,
"antiCheatFlagsJson": snapshot.anti_cheat_flags_json,
"leaderboardScore": snapshot.leaderboard_score,
"scoreId": snapshot.score_id,
})
}

View File

@@ -0,0 +1,616 @@
use super::*;
pub(crate) fn map_big_fish_session_procedure_result(
result: BigFishSessionProcedureResult,
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish session 快照"))?;
Ok(map_big_fish_session_snapshot(session))
}
pub(crate) fn map_big_fish_works_procedure_result(
result: BigFishWorksProcedureResult,
_fallback_owner_user_id: Option<&str>,
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.items
.into_iter()
.map(map_big_fish_work_summary_snapshot)
.collect())
}
pub(crate) fn map_big_fish_run_procedure_result(
result: BigFishRunProcedureResult,
) -> Result<BigFishRuntimeRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish run 快照"))?;
Ok(map_big_fish_runtime_snapshot(run))
}
pub(crate) fn map_big_fish_session_snapshot(
snapshot: BigFishSessionSnapshot,
) -> BigFishSessionRecord {
BigFishSessionRecord {
session_id: snapshot.session_id,
current_turn: snapshot.current_turn,
progress_percent: snapshot.progress_percent,
stage: format_big_fish_creation_stage(snapshot.stage).to_string(),
anchor_pack: map_big_fish_anchor_pack(snapshot.anchor_pack),
draft: snapshot.draft.map(map_big_fish_game_draft),
asset_slots: snapshot
.asset_slots
.into_iter()
.map(map_big_fish_asset_slot_snapshot)
.collect(),
asset_coverage: map_big_fish_asset_coverage(snapshot.asset_coverage),
messages: snapshot
.messages
.into_iter()
.map(map_big_fish_agent_message_snapshot)
.collect(),
last_assistant_reply: snapshot.last_assistant_reply,
publish_ready: snapshot.publish_ready,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn map_big_fish_anchor_pack(snapshot: BigFishAnchorPack) -> BigFishAnchorPackRecord {
BigFishAnchorPackRecord {
gameplay_promise: map_big_fish_anchor_item(snapshot.gameplay_promise),
ecology_visual_theme: map_big_fish_anchor_item(snapshot.ecology_visual_theme),
growth_ladder: map_big_fish_anchor_item(snapshot.growth_ladder),
risk_tempo: map_big_fish_anchor_item(snapshot.risk_tempo),
}
}
pub(crate) fn map_big_fish_anchor_item(snapshot: BigFishAnchorItem) -> BigFishAnchorItemRecord {
BigFishAnchorItemRecord {
key: snapshot.key,
label: snapshot.label,
value: snapshot.value,
status: format_big_fish_anchor_status(snapshot.status).to_string(),
}
}
pub(crate) fn map_big_fish_game_draft(snapshot: BigFishGameDraft) -> BigFishGameDraftRecord {
BigFishGameDraftRecord {
title: snapshot.title,
subtitle: snapshot.subtitle,
core_fun: snapshot.core_fun,
ecology_theme: snapshot.ecology_theme,
levels: snapshot
.levels
.into_iter()
.map(map_big_fish_level_blueprint)
.collect(),
background: map_big_fish_background_blueprint(snapshot.background),
runtime_params: map_big_fish_runtime_params(snapshot.runtime_params),
}
}
pub(crate) fn map_big_fish_level_blueprint(
snapshot: BigFishLevelBlueprint,
) -> BigFishLevelBlueprintRecord {
BigFishLevelBlueprintRecord {
level: snapshot.level,
name: snapshot.name,
one_line_fantasy: snapshot.one_line_fantasy,
text_description: snapshot.text_description,
silhouette_direction: snapshot.silhouette_direction,
size_ratio: snapshot.size_ratio,
visual_description: snapshot.visual_description,
visual_prompt_seed: snapshot.visual_prompt_seed,
idle_motion_description: snapshot.idle_motion_description,
move_motion_description: snapshot.move_motion_description,
motion_prompt_seed: snapshot.motion_prompt_seed,
merge_source_level: snapshot.merge_source_level,
prey_window: snapshot.prey_window,
threat_window: snapshot.threat_window,
is_final_level: snapshot.is_final_level,
}
}
pub(crate) fn map_big_fish_background_blueprint(
snapshot: BigFishBackgroundBlueprint,
) -> BigFishBackgroundBlueprintRecord {
BigFishBackgroundBlueprintRecord {
theme: snapshot.theme,
color_mood: snapshot.color_mood,
foreground_hints: snapshot.foreground_hints,
midground_composition: snapshot.midground_composition,
background_depth: snapshot.background_depth,
safe_play_area_hint: snapshot.safe_play_area_hint,
spawn_edge_hint: snapshot.spawn_edge_hint,
background_prompt_seed: snapshot.background_prompt_seed,
}
}
pub(crate) fn map_big_fish_runtime_params(
snapshot: BigFishRuntimeParams,
) -> BigFishRuntimeParamsRecord {
BigFishRuntimeParamsRecord {
level_count: snapshot.level_count,
merge_count_per_upgrade: snapshot.merge_count_per_upgrade,
spawn_target_count: snapshot.spawn_target_count,
leader_move_speed: snapshot.leader_move_speed,
follower_catch_up_speed: snapshot.follower_catch_up_speed,
offscreen_cull_seconds: snapshot.offscreen_cull_seconds,
prey_spawn_delta_levels: snapshot.prey_spawn_delta_levels,
threat_spawn_delta_levels: snapshot.threat_spawn_delta_levels,
win_level: snapshot.win_level,
}
}
pub(crate) fn map_big_fish_asset_slot_snapshot(
snapshot: BigFishAssetSlotSnapshot,
) -> BigFishAssetSlotRecord {
BigFishAssetSlotRecord {
slot_id: snapshot.slot_id,
asset_kind: format_big_fish_asset_kind(snapshot.asset_kind).to_string(),
level: snapshot.level,
motion_key: snapshot.motion_key,
status: format_big_fish_asset_status(snapshot.status).to_string(),
asset_url: snapshot.asset_url,
prompt_snapshot: snapshot.prompt_snapshot,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn map_big_fish_asset_coverage(
snapshot: BigFishAssetCoverage,
) -> BigFishAssetCoverageRecord {
BigFishAssetCoverageRecord {
level_main_image_ready_count: snapshot.level_main_image_ready_count,
level_motion_ready_count: snapshot.level_motion_ready_count,
background_ready: snapshot.background_ready,
required_level_count: snapshot.required_level_count,
publish_ready: snapshot.publish_ready,
blockers: snapshot.blockers,
}
}
pub(crate) fn map_big_fish_agent_message_snapshot(
snapshot: BigFishAgentMessageSnapshot,
) -> BigFishAgentMessageRecord {
BigFishAgentMessageRecord {
message_id: snapshot.message_id,
role: format_big_fish_agent_message_role(snapshot.role).to_string(),
kind: format_big_fish_agent_message_kind(snapshot.kind).to_string(),
text: snapshot.text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_big_fish_work_summary_snapshot(
snapshot: BigFishWorkSummarySnapshot,
) -> BigFishWorkSummaryRecord {
BigFishWorkSummaryRecord {
work_id: snapshot.work_id,
source_session_id: snapshot.source_session_id,
owner_user_id: snapshot.owner_user_id,
title: snapshot.title,
subtitle: snapshot.subtitle,
summary: snapshot.summary,
cover_image_src: snapshot.cover_image_src,
status: snapshot.status,
updated_at_micros: snapshot.updated_at_micros,
published_at_micros: snapshot.published_at_micros,
publish_ready: snapshot.publish_ready,
level_count: snapshot.level_count,
level_main_image_ready_count: snapshot.level_main_image_ready_count,
level_motion_ready_count: snapshot.level_motion_ready_count,
background_ready: snapshot.background_ready,
play_count: snapshot.play_count,
remix_count: snapshot.remix_count,
like_count: snapshot.like_count,
recent_play_count_7d: snapshot.recent_play_count_7_d,
}
}
pub(crate) fn map_big_fish_gallery_view_row(
row: BigFishWorkSummarySnapshot,
recent_play_count_7d: u32,
) -> BigFishWorkSummaryRecord {
let mut record = map_big_fish_work_summary_snapshot(row);
record.recent_play_count_7d = recent_play_count_7d;
record
}
pub(crate) fn map_big_fish_runtime_snapshot(
snapshot: BigFishRuntimeSnapshot,
) -> BigFishRuntimeRunRecord {
BigFishRuntimeRunRecord {
run_id: snapshot.run_id,
session_id: snapshot.session_id,
status: format_big_fish_run_status(snapshot.status).to_string(),
tick: snapshot.tick,
player_level: snapshot.player_level,
win_level: snapshot.win_level,
leader_entity_id: snapshot.leader_entity_id,
owned_entities: snapshot
.owned_entities
.into_iter()
.map(map_big_fish_runtime_entity_snapshot)
.collect(),
wild_entities: snapshot
.wild_entities
.into_iter()
.map(map_big_fish_runtime_entity_snapshot)
.collect(),
camera_center: map_big_fish_vector2(snapshot.camera_center),
last_input: map_big_fish_vector2(snapshot.last_input),
event_log: snapshot.event_log,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_big_fish_runtime_entity_snapshot(
snapshot: BigFishRuntimeEntitySnapshot,
) -> BigFishRuntimeEntityRecord {
BigFishRuntimeEntityRecord {
entity_id: snapshot.entity_id,
level: snapshot.level,
position: map_big_fish_vector2(snapshot.position),
radius: snapshot.radius,
offscreen_seconds: snapshot.offscreen_seconds,
}
}
fn map_big_fish_vector2(snapshot: BigFishVector2) -> BigFishVector2Record {
BigFishVector2Record {
x: snapshot.x,
y: snapshot.y,
}
}
pub(crate) fn parse_big_fish_creation_stage(
value: &str,
) -> Result<BigFishCreationStage, SpacetimeClientError> {
match value.trim() {
"collecting_anchors" => Ok(BigFishCreationStage::CollectingAnchors),
"draft_ready" => Ok(BigFishCreationStage::DraftReady),
"asset_refining" => Ok(BigFishCreationStage::AssetRefining),
"ready_to_publish" => Ok(BigFishCreationStage::ReadyToPublish),
"published" => Ok(BigFishCreationStage::Published),
other => Err(SpacetimeClientError::Runtime(format!(
"big fish creation stage `{other}` 当前尚未支持"
))),
}
}
pub(crate) fn format_big_fish_creation_stage(value: BigFishCreationStage) -> &'static str {
match value {
BigFishCreationStage::CollectingAnchors => "collecting_anchors",
BigFishCreationStage::DraftReady => "draft_ready",
BigFishCreationStage::AssetRefining => "asset_refining",
BigFishCreationStage::ReadyToPublish => "ready_to_publish",
BigFishCreationStage::Published => "published",
}
}
pub(crate) fn format_big_fish_anchor_status(value: BigFishAnchorStatus) -> &'static str {
match value {
BigFishAnchorStatus::Confirmed => "confirmed",
BigFishAnchorStatus::Inferred => "inferred",
BigFishAnchorStatus::Missing => "missing",
BigFishAnchorStatus::Locked => "locked",
}
}
pub(crate) fn format_big_fish_agent_message_role(value: BigFishAgentMessageRole) -> &'static str {
match value {
BigFishAgentMessageRole::User => "user",
BigFishAgentMessageRole::Assistant => "assistant",
BigFishAgentMessageRole::System => "system",
}
}
pub(crate) fn format_big_fish_agent_message_kind(value: BigFishAgentMessageKind) -> &'static str {
match value {
BigFishAgentMessageKind::Chat => "chat",
BigFishAgentMessageKind::Summary => "summary",
BigFishAgentMessageKind::ActionResult => "action_result",
BigFishAgentMessageKind::Warning => "warning",
}
}
pub(crate) fn format_big_fish_asset_kind(value: BigFishAssetKind) -> &'static str {
match value {
BigFishAssetKind::LevelMainImage => "level_main_image",
BigFishAssetKind::LevelMotion => "level_motion",
BigFishAssetKind::StageBackground => "stage_background",
}
}
pub(crate) fn format_big_fish_asset_status(value: BigFishAssetStatus) -> &'static str {
match value {
BigFishAssetStatus::Missing => "missing",
BigFishAssetStatus::Ready => "ready",
}
}
pub(crate) fn format_big_fish_run_status(value: BigFishRunStatus) -> &'static str {
match value {
BigFishRunStatus::Running => "running",
BigFishRunStatus::Won => "won",
BigFishRunStatus::Failed => "failed",
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct BigFishWorkSummaryRecord {
pub work_id: String,
pub source_session_id: String,
pub owner_user_id: String,
pub title: String,
pub subtitle: String,
pub summary: String,
pub cover_image_src: Option<String>,
pub status: String,
pub updated_at_micros: i64,
pub published_at_micros: Option<i64>,
pub publish_ready: bool,
pub level_count: u32,
pub level_main_image_ready_count: u32,
pub level_motion_ready_count: u32,
pub background_ready: bool,
pub play_count: u32,
pub remix_count: u32,
pub like_count: u32,
pub recent_play_count_7d: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn puzzle_works_mapper_keeps_typed_public_stat_fields() {
let result = PuzzleWorksProcedureResult {
ok: true,
items: vec![PuzzleWorkProfile {
work_id: "puzzle-work-1".to_string(),
profile_id: "puzzle-profile-1".to_string(),
owner_user_id: "user-1".to_string(),
source_session_id: None,
author_display_name: "测试作者".to_string(),
work_title: "雨夜拼图作品".to_string(),
work_description: "拼图作品说明".to_string(),
level_name: "雨夜拼图".to_string(),
summary: "公开作品摘要".to_string(),
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()],
cover_image_src: None,
cover_asset_id: None,
levels: Vec::new(),
publication_status: PuzzlePublicationStatus::Published,
updated_at_micros: 123000000,
published_at_micros: Some(123000000),
play_count: 11,
remix_count: 7,
like_count: 5,
recent_play_count_7_d: 3,
point_incentive_total_half_points: 4,
point_incentive_claimed_points: 2,
publish_ready: true,
anchor_pack: test_puzzle_anchor_pack(),
}],
error_message: None,
};
let items = map_puzzle_works_procedure_result(result)
.expect("typed puzzle works result 应能映射统计字段");
assert_eq!(items.len(), 1);
assert_eq!(items[0].play_count, 11);
assert_eq!(items[0].remix_count, 7);
assert_eq!(items[0].like_count, 5);
assert_eq!(items[0].recent_play_count_7d, 3);
}
#[test]
fn puzzle_run_mapper_maps_typed_timer_fields() {
let result = PuzzleRunProcedureResult {
ok: true,
run: Some(PuzzleRunSnapshot {
run_id: "puzzle-run-1".to_string(),
entry_profile_id: "puzzle-profile-1".to_string(),
cleared_level_count: 0,
current_level_index: 1,
current_grid_size: 3,
played_profile_ids: vec!["puzzle-profile-1".to_string()],
previous_level_tags: vec![
"雨夜".to_string(),
"猫咪".to_string(),
"神庙".to_string(),
],
current_level: Some(PuzzleRuntimeLevelSnapshot {
run_id: "puzzle-run-1".to_string(),
level_index: 1,
level_id: None,
grid_size: 3,
profile_id: "puzzle-profile-1".to_string(),
level_name: "雨夜拼图".to_string(),
author_display_name: "测试作者".to_string(),
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()],
cover_image_src: None,
ui_background_image_src: None,
ui_background_image_object_key: None,
background_music: None,
board: PuzzleBoardSnapshot {
rows: 3,
cols: 3,
pieces: vec![PuzzlePieceState {
piece_id: "piece-1".to_string(),
correct_row: 0,
correct_col: 0,
current_row: 0,
current_col: 0,
merged_group_id: None,
}],
merged_groups: Vec::new(),
selected_piece_id: None,
all_tiles_resolved: false,
},
status: PuzzleRuntimeLevelStatus::Playing,
started_at_ms: 0,
cleared_at_ms: None,
elapsed_ms: None,
time_limit_ms: 0,
remaining_ms: 0,
paused_accumulated_ms: 0,
pause_started_at_ms: None,
freeze_accumulated_ms: 0,
freeze_started_at_ms: None,
freeze_until_ms: None,
leaderboard_entries: Vec::new(),
}),
recommended_next_profile_id: None,
next_level_mode: "none".to_string(),
next_level_profile_id: None,
next_level_id: None,
recommended_next_works: Vec::new(),
leaderboard_entries: Vec::new(),
}),
error_message: None,
};
let run = map_puzzle_run_procedure_result(result)
.expect("typed puzzle run result 应能映射计时字段");
let level = run.current_level.expect("兼容后仍应保留当前关卡");
assert_eq!(run.run_id, "puzzle-run-1");
assert!(level.started_at_ms > 0);
assert_eq!(level.time_limit_ms, 0);
assert_eq!(level.remaining_ms, 0);
assert!(level.leaderboard_entries.is_empty());
}
#[test]
fn big_fish_works_mapper_uses_typed_owner_and_public_stats() {
let result = BigFishWorksProcedureResult {
ok: true,
items: vec![BigFishWorkSummarySnapshot {
work_id: "big-fish-work-session-1".to_string(),
source_session_id: "session-1".to_string(),
owner_user_id: "user-1".to_string(),
title: "深海草稿".to_string(),
subtitle: "副标题".to_string(),
summary: "摘要".to_string(),
cover_image_src: None,
status: "draft".to_string(),
updated_at_micros: 123,
publish_ready: false,
level_count: 8,
level_main_image_ready_count: 0,
level_motion_ready_count: 0,
background_ready: false,
play_count: 9,
remix_count: 4,
like_count: 2,
recent_play_count_7_d: 6,
published_at_micros: None,
}],
error_message: None,
};
let items = map_big_fish_works_procedure_result(result, Some("user-1"))
.expect("typed big fish works result 应能映射 owner 和统计字段");
assert_eq!(items.len(), 1);
assert_eq!(items[0].owner_user_id, "user-1");
assert_eq!(items[0].published_at_micros, None);
assert_eq!(items[0].play_count, 9);
assert_eq!(items[0].remix_count, 4);
assert_eq!(items[0].like_count, 2);
assert_eq!(items[0].recent_play_count_7d, 6);
}
#[test]
fn match3d_work_mapper_keeps_generated_item_assets_json() {
let result = Match3DWorkProcedureResult {
ok: true,
work: Some(Match3DWorkSnapshot {
profile_id: "match3d-profile-1".to_string(),
owner_user_id: "user-1".to_string(),
source_session_id: "match3d-session-1".to_string(),
author_display_name: "测试作者".to_string(),
game_name: "水果抓大鹅".to_string(),
theme_text: "水果".to_string(),
summary_text: "水果主题".to_string(),
tags: vec!["水果".to_string()],
cover_image_src: String::new(),
cover_asset_id: String::new(),
clear_count: 3,
difficulty: 3,
config: Match3DCreatorConfigSnapshot {
theme_text: "水果".to_string(),
reference_image_src: None,
clear_count: 3,
difficulty: 3,
asset_style_id: None,
asset_style_label: None,
asset_style_prompt: None,
generate_click_sound: false,
},
publication_status: "Draft".to_string(),
publish_ready: false,
play_count: 0,
updated_at_micros: 123000000,
published_at_micros: None,
generated_item_assets_json: Some(
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
.to_string(),
),
}),
error_message: None,
};
let item = map_match3d_work_procedure_result(result)
.expect("typed match3d work result 应保留生成素材 JSON");
assert_eq!(
item.generated_item_assets_json.as_deref(),
Some(
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
)
);
}
fn test_puzzle_anchor_pack() -> PuzzleAnchorPack {
PuzzleAnchorPack {
theme_promise: test_puzzle_anchor_item("themePromise", "题材承诺", "雨夜冒险"),
visual_subject: test_puzzle_anchor_item("visualSubject", "画面主体", "猫咪神庙"),
visual_mood: test_puzzle_anchor_item("visualMood", "视觉气质", "温暖"),
composition_hooks: test_puzzle_anchor_item("compositionHooks", "拼图记忆点", "灯光"),
tags_and_forbidden: test_puzzle_anchor_item(
"tagsAndForbidden",
"标签与禁忌",
"雨夜, 猫咪, 神庙",
),
}
}
fn test_puzzle_anchor_item(key: &str, label: &str, value: &str) -> PuzzleAnchorItem {
PuzzleAnchorItem {
key: key.to_string(),
label: label.to_string(),
value: value.to_string(),
status: PuzzleAnchorStatus::Inferred,
}
}
}

View File

@@ -0,0 +1,124 @@
use super::*;
impl From<DomainBattleStateQueryInput> for BattleStateQueryInput {
fn from(input: DomainBattleStateQueryInput) -> Self {
Self {
battle_state_id: input.battle_state_id,
}
}
}
impl From<DomainResolveCombatActionInput> for ResolveCombatActionInput {
fn from(input: DomainResolveCombatActionInput) -> Self {
Self {
battle_state_id: input.battle_state_id,
function_id: input.function_id,
action_text: input.action_text,
base_damage: input.base_damage,
mana_cost: input.mana_cost,
heal: input.heal,
mana_restore: input.mana_restore,
counter_multiplier_basis_points: input.counter_multiplier_basis_points,
updated_at_micros: input.updated_at_micros,
}
}
}
pub type BarkBattleDraftConfigRecord = serde_json::Value;
pub type BarkBattleRuntimeConfigRecord = serde_json::Value;
pub type BarkBattleRunRecord = serde_json::Value;
pub(crate) fn map_battle_state_procedure_result(
result: BattleStateProcedureResult,
) -> Result<BattleStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.snapshot
.ok_or_else(|| SpacetimeClientError::missing_snapshot("battle_state 快照"))?;
Ok(build_battle_state_record(map_battle_state_snapshot(
snapshot,
)))
}
pub(crate) fn map_resolve_combat_action_procedure_result(
result: ResolveCombatActionProcedureResult,
) -> Result<ResolveCombatActionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let action_result = result
.result
.ok_or_else(|| SpacetimeClientError::missing_snapshot("战斗结算结果"))?;
Ok(build_resolve_combat_action_record(
map_resolve_combat_action_result(action_result),
))
}
pub(crate) fn map_resolve_combat_action_result(
result: ResolveCombatActionResult,
) -> DomainResolveCombatActionResult {
DomainResolveCombatActionResult {
snapshot: map_battle_state_snapshot(result.snapshot),
damage_dealt: result.damage_dealt,
damage_taken: result.damage_taken,
outcome: map_combat_outcome(result.outcome),
}
}
pub(crate) fn map_battle_mode(value: DomainBattleMode) -> BattleMode {
match value {
DomainBattleMode::Fight => BattleMode::Fight,
DomainBattleMode::Spar => BattleMode::Spar,
}
}
pub(crate) fn map_battle_mode_back(value: BattleMode) -> DomainBattleMode {
match value {
BattleMode::Fight => DomainBattleMode::Fight,
BattleMode::Spar => DomainBattleMode::Spar,
}
}
pub(crate) fn map_battle_status(value: BattleStatus) -> DomainBattleStatus {
match value {
BattleStatus::Ongoing => DomainBattleStatus::Ongoing,
BattleStatus::Resolved => DomainBattleStatus::Resolved,
BattleStatus::Aborted => DomainBattleStatus::Aborted,
}
}
pub(crate) fn map_combat_outcome(value: CombatOutcome) -> DomainCombatOutcome {
match value {
CombatOutcome::Ongoing => DomainCombatOutcome::Ongoing,
CombatOutcome::Victory => DomainCombatOutcome::Victory,
CombatOutcome::SparComplete => DomainCombatOutcome::SparComplete,
CombatOutcome::Escaped => DomainCombatOutcome::Escaped,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolveCombatActionRecord {
pub battle_state: BattleStateRecord,
pub damage_dealt: i32,
pub damage_taken: i32,
pub outcome: String,
}
pub(crate) fn build_resolve_combat_action_record(
result: DomainResolveCombatActionResult,
) -> ResolveCombatActionRecord {
ResolveCombatActionRecord {
battle_state: build_battle_state_record(result.snapshot),
damage_dealt: result.damage_dealt,
damage_taken: result.damage_taken,
outcome: result.outcome.as_str().to_string(),
}
}

View File

@@ -0,0 +1,706 @@
use super::*;
impl From<CustomWorldPublishWorldRecordInput> for CustomWorldPublishWorldInput {
fn from(input: CustomWorldPublishWorldRecordInput) -> Self {
Self {
session_id: input.session_id,
profile_id: input.profile_id,
owner_user_id: input.owner_user_id,
public_work_code: input.public_work_code,
author_public_user_code: input.author_public_user_code,
draft_profile_json: input.draft_profile_json,
legacy_result_profile_json: input.legacy_result_profile_json,
setting_text: input.setting_text,
author_display_name: input.author_display_name,
published_at_micros: input.published_at_micros,
}
}
}
pub(crate) fn empty_string_to_none(value: String) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
pub(crate) fn i64_to_u64_ms(value: i64) -> u64 {
value.max(0) as u64
}
pub(crate) fn parse_optional_json_value(
value: Option<&str>,
fallback: serde_json::Value,
label: &str,
) -> Result<serde_json::Value, SpacetimeClientError> {
match value.map(str::trim).filter(|value| !value.is_empty()) {
Some(value) => parse_json_value(value, label),
None => Ok(fallback),
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldLibraryMutationRecord {
pub entry: CustomWorldLibraryEntryRecord,
pub gallery_entry: Option<CustomWorldGalleryEntryRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldPublishWorldRecord {
pub compiled_record: CustomWorldPublishedProfileCompileRecord,
pub entry: CustomWorldLibraryEntryRecord,
pub gallery_entry: Option<CustomWorldGalleryEntryRecord>,
pub session_stage: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldAgentMessageRecord {
pub message_id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
pub related_operation_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldAgentOperationRecord {
pub operation_id: String,
pub operation_type: String,
pub status: String,
pub phase_label: String,
pub phase_detail: String,
pub progress: u32,
pub error_message: Option<String>,
pub started_at_micros: i64,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentOperationProgressRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub operation_id: String,
// SpacetimeDB 模块侧使用枚举存储操作类型,这里保留字符串给 API 层做轻量传参。
pub operation_type: String,
pub operation_status: String,
pub phase_label: String,
pub phase_detail: String,
pub operation_progress: u32,
pub error_message: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldSupportedActionRecord {
pub action: String,
pub enabled: bool,
pub reason: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldCheckpointRecord {
pub checkpoint_id: String,
pub created_at: String,
pub label: String,
}
// 兼容并行 custom world facade 中仍在使用的旧命名,避免本轮 module-npc 收口被无关改动阻塞。
pub type CustomWorldAgentCheckpointRecord = CustomWorldCheckpointRecord;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldResultPreviewBlockerRecord {
pub id: String,
pub code: String,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldPublishGateRecord {
pub profile_id: String,
pub blockers: Vec<CustomWorldResultPreviewBlockerRecord>,
pub blocker_count: u32,
pub publish_ready: bool,
pub can_enter_world: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldDraftCardDetailSectionRecord {
pub section_id: String,
pub label: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldProfileRemixRecordInput {
pub source_owner_user_id: String,
pub source_profile_id: String,
pub target_owner_user_id: String,
pub target_profile_id: String,
pub author_display_name: String,
pub remixed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldProfilePlayReportRecordInput {
pub owner_user_id: String,
pub profile_id: String,
pub played_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldProfileLikeReportRecordInput {
pub owner_user_id: String,
pub profile_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldPublishWorldRecordInput {
pub session_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub public_work_code: Option<String>,
pub author_public_user_code: String,
pub draft_profile_json: String,
pub legacy_result_profile_json: Option<String>,
pub setting_text: String,
pub author_display_name: String,
pub published_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentMessageSubmitRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub user_message_id: String,
pub user_message_text: String,
pub operation_id: String,
pub submitted_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldAgentActionExecuteRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub operation_id: String,
pub action: String,
pub payload_json: Option<String>,
pub submitted_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldAgentActionExecuteRecord {
pub operation: CustomWorldAgentOperationRecord,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishPlayReportRecordInput {
pub session_id: String,
pub user_id: String,
pub elapsed_ms: u64,
pub reported_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishRunStartRecordInput {
pub run_id: String,
pub session_id: String,
pub owner_user_id: String,
pub started_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishInputSubmitRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub x: f32,
pub y: f32,
pub submitted_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishLikeReportRecordInput {
pub session_id: String,
pub user_id: String,
pub liked_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishWorkRemixRecordInput {
pub source_session_id: String,
pub target_session_id: String,
pub target_owner_user_id: String,
pub welcome_message_id: String,
pub remixed_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAgentSessionCreateRecordInput {
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<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAgentMessageSubmitRecordInput {
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 SquareHoleAgentMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub config_json: Option<String>,
pub progress_percent: u32,
pub stage: String,
pub updated_at_micros: i64,
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleCompileDraftRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub author_display_name: String,
pub game_name: Option<String>,
pub summary_text: Option<String>,
pub tags_json: Option<String>,
pub cover_image_src: Option<String>,
pub compiled_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleWorkUpdateRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub game_name: String,
pub theme_text: String,
pub twist_rule: String,
pub summary_text: String,
pub tags_json: String,
pub cover_image_src: String,
pub background_prompt: String,
pub background_image_src: String,
pub shape_options_json: String,
pub hole_options_json: String,
pub shape_count: u32,
pub difficulty: u32,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleRunStartRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub started_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleRunDropRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub hole_id: String,
pub client_snapshot_version: u64,
pub client_event_id: String,
pub dropped_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleRunStopRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub stopped_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleRunRestartRecordInput {
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 SquareHoleRunTimeUpRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub finished_at_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelAgentMessageSubmitRecordInput {
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 VisualNovelAgentMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub draft_json: Option<String>,
pub pending_action_json: Option<String>,
pub status: String,
pub progress_percent: u32,
pub updated_at_micros: i64,
pub error_message: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelWorkCompileRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub work_id: Option<String>,
pub author_display_name: String,
pub work_title: Option<String>,
pub work_description: Option<String>,
pub tags_json: Option<String>,
pub cover_image_src: Option<String>,
pub compiled_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRunStartRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub snapshot_json: Option<String>,
pub started_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRunSnapshotRecordInput {
pub run_id: String,
pub owner_user_id: String,
pub status: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids_json: String,
pub flags_json: String,
pub metrics_json: String,
pub available_choices_json: String,
pub text_mode_enabled: bool,
pub snapshot_json: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelHistoryEntryRecordInput {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: Option<String>,
pub steps_json: String,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelAgentMessageRecord {
pub message_id: String,
pub session_id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelHistoryEntryRecord {
pub entry_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub turn_index: u32,
pub source: String,
pub action_text: Option<String>,
pub steps: serde_json::Value,
pub snapshot_before_hash: Option<String>,
pub snapshot_after_hash: Option<String>,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelRunRecord {
pub run_id: String,
pub owner_user_id: String,
pub profile_id: String,
pub mode: String,
pub status: String,
pub current_scene_id: Option<String>,
pub current_phase_id: Option<String>,
pub visible_character_ids: Vec<String>,
pub flags: serde_json::Value,
pub metrics: serde_json::Value,
pub history: Vec<VisualNovelHistoryEntryRecord>,
pub available_choices: serde_json::Value,
pub text_mode_enabled: bool,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAnchorItemRecord {
pub key: String,
pub label: String,
pub value: String,
pub status: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAnchorPackRecord {
pub theme: SquareHoleAnchorItemRecord,
pub twist_rule: SquareHoleAnchorItemRecord,
pub shape_count: SquareHoleAnchorItemRecord,
pub difficulty: SquareHoleAnchorItemRecord,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleCreatorConfigRecord {
pub theme_text: String,
pub twist_rule: String,
pub shape_count: u32,
pub difficulty: u32,
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
pub background_prompt: String,
pub cover_image_src: Option<String>,
pub background_image_src: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleShapeOptionRecord {
pub option_id: String,
pub shape_kind: String,
pub label: String,
pub target_hole_id: String,
pub image_prompt: String,
pub image_src: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleHoleOptionRecord {
pub hole_id: String,
pub hole_kind: String,
pub label: String,
pub image_prompt: String,
pub image_src: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleResultDraftRecord {
pub profile_id: String,
pub game_name: String,
pub theme_text: String,
pub twist_rule: String,
pub summary: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub background_prompt: String,
pub background_image_src: Option<String>,
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
pub shape_count: u32,
pub difficulty: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAgentMessageRecord {
pub id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleAgentSessionRecord {
pub session_id: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
pub anchor_pack: SquareHoleAnchorPackRecord,
pub config: SquareHoleCreatorConfigRecord,
pub draft: Option<SquareHoleResultDraftRecord>,
pub messages: Vec<SquareHoleAgentMessageRecord>,
pub last_assistant_reply: Option<String>,
pub published_profile_id: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleWorkProfileRecord {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub source_session_id: Option<String>,
pub author_display_name: String,
pub game_name: String,
pub theme_text: String,
pub twist_rule: String,
pub summary: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub background_prompt: String,
pub background_image_src: Option<String>,
pub shape_options: Vec<SquareHoleShapeOptionRecord>,
pub hole_options: Vec<SquareHoleHoleOptionRecord>,
pub shape_count: u32,
pub difficulty: u32,
pub publication_status: String,
pub play_count: u32,
pub updated_at: String,
pub published_at: Option<String>,
pub publish_ready: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareHoleShapeSnapshotRecord {
pub shape_id: String,
pub shape_kind: String,
pub label: String,
pub target_hole_id: String,
pub color: String,
pub image_src: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SquareHoleHoleSnapshotRecord {
pub hole_id: String,
pub hole_kind: String,
pub label: String,
pub x: f32,
pub y: f32,
pub image_src: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishSessionCreateRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub seed_text: String,
pub welcome_message_id: String,
pub welcome_message_text: String,
pub created_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishMessageSubmitRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub user_message_id: String,
pub user_message_text: String,
pub assistant_message_id: String,
pub submitted_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishMessageFinalizeRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub assistant_message_id: Option<String>,
pub assistant_reply_text: Option<String>,
pub stage: String,
pub progress_percent: u32,
pub anchor_pack_json: String,
pub error_message: Option<String>,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishDraftCompileRecordInput {
pub session_id: String,
pub owner_user_id: String,
pub draft_json: Option<String>,
pub compiled_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAnchorItemRecord {
pub key: String,
pub label: String,
pub value: String,
pub status: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAnchorPackRecord {
pub gameplay_promise: BigFishAnchorItemRecord,
pub ecology_visual_theme: BigFishAnchorItemRecord,
pub growth_ladder: BigFishAnchorItemRecord,
pub risk_tempo: BigFishAnchorItemRecord,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishLevelBlueprintRecord {
pub level: u32,
pub name: String,
pub one_line_fantasy: String,
pub text_description: String,
pub silhouette_direction: String,
pub size_ratio: f32,
pub visual_description: String,
pub visual_prompt_seed: String,
pub idle_motion_description: String,
pub move_motion_description: String,
pub motion_prompt_seed: String,
pub merge_source_level: Option<u32>,
pub prey_window: Vec<u32>,
pub threat_window: Vec<u32>,
pub is_final_level: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishBackgroundBlueprintRecord {
pub theme: String,
pub color_mood: String,
pub foreground_hints: String,
pub midground_composition: String,
pub background_depth: String,
pub safe_play_area_hint: String,
pub spawn_edge_hint: String,
pub background_prompt_seed: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigFishAgentMessageRecord {
pub message_id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishVector2Record {
pub x: f32,
pub y: f32,
}

View File

@@ -0,0 +1,957 @@
use super::*;
impl From<CustomWorldProfileUpsertRecordInput> for CustomWorldProfileUpsertInput {
fn from(input: CustomWorldProfileUpsertRecordInput) -> Self {
Self {
profile_id: input.profile_id,
owner_user_id: input.owner_user_id,
public_work_code: input.public_work_code,
author_public_user_code: input.author_public_user_code,
source_agent_session_id: input.source_agent_session_id,
world_name: input.world_name,
subtitle: input.subtitle,
summary_text: input.summary_text,
theme_mode: map_custom_world_theme_mode(input.theme_mode),
cover_image_src: input.cover_image_src,
profile_payload_json: input.profile_payload_json,
playable_npc_count: input.playable_npc_count,
landmark_count: input.landmark_count,
author_display_name: input.author_display_name,
updated_at_micros: input.updated_at_micros,
}
}
}
pub(crate) fn map_custom_world_profile_list_result(
result: CustomWorldProfileListResult,
) -> Result<Vec<CustomWorldLibraryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.entries
.into_iter()
.map(map_custom_world_library_entry_from_profile_snapshot)
.collect()
}
pub(crate) fn map_custom_world_library_detail_result(
result: CustomWorldLibraryMutationResult,
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let entry = result
.entry
.ok_or_else(|| SpacetimeClientError::Procedure("custom_world_profile 不存在".to_string()))
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
let gallery_entry = result
.gallery_entry
.map(map_custom_world_gallery_entry_snapshot)
.transpose()?;
Ok(CustomWorldLibraryMutationRecord {
entry,
gallery_entry,
})
}
pub(crate) fn map_custom_world_gallery_list_result(
result: CustomWorldGalleryListResult,
) -> Result<Vec<CustomWorldGalleryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(map_custom_world_gallery_entry_snapshot)
.collect::<Result<Vec<_>, _>>()?)
}
pub(crate) fn map_custom_world_library_mutation_result(
result: CustomWorldLibraryMutationResult,
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let entry = result
.entry
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world entry"))
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
let gallery_entry = result
.gallery_entry
.map(map_custom_world_gallery_entry_snapshot)
.transpose()?;
Ok(CustomWorldLibraryMutationRecord {
entry,
gallery_entry,
})
}
pub(crate) fn map_custom_world_publish_world_result(
result: CustomWorldPublishWorldResult,
) -> Result<CustomWorldPublishWorldRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let compiled_record = result
.compiled_record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("published profile compile 快照"))
.and_then(map_custom_world_published_profile_compile_snapshot)?;
let entry = result
.entry
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world entry"))
.and_then(map_custom_world_library_entry_from_profile_snapshot)?;
let gallery_entry = result
.gallery_entry
.map(map_custom_world_gallery_entry_snapshot)
.transpose()?;
let session_stage = result
.session_stage
.ok_or_else(|| SpacetimeClientError::missing_snapshot("session stage"))
.map(map_rpg_agent_stage)?;
Ok(CustomWorldPublishWorldRecord {
compiled_record,
entry,
gallery_entry,
session_stage,
})
}
pub(crate) fn map_custom_world_agent_session_procedure_result(
result: CustomWorldAgentSessionProcedureResult,
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world agent session 快照"))?;
map_custom_world_agent_session_snapshot(session)
}
pub(crate) fn map_custom_world_agent_operation_procedure_result(
result: CustomWorldAgentOperationProcedureResult,
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let operation = result.operation.ok_or_else(|| {
SpacetimeClientError::missing_snapshot("custom world agent operation 快照")
})?;
Ok(map_custom_world_agent_operation_snapshot(operation))
}
pub(crate) fn map_custom_world_works_list_result(
result: CustomWorldWorksListResult,
) -> Result<Vec<CustomWorldWorkSummaryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.items
.into_iter()
.map(map_custom_world_work_summary_snapshot)
.collect()
}
pub(crate) fn map_custom_world_draft_card_detail_result(
result: CustomWorldDraftCardDetailResult,
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let card = result
.card
.ok_or_else(|| SpacetimeClientError::missing_snapshot("custom world card detail 快照"))?;
map_custom_world_draft_card_detail_snapshot(card)
}
pub(crate) fn map_custom_world_agent_action_execute_result(
result: CustomWorldAgentActionExecuteResult,
) -> Result<CustomWorldAgentActionExecuteRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let operation = result.operation.ok_or_else(|| {
SpacetimeClientError::missing_snapshot("custom world action operation 快照")
})?;
Ok(CustomWorldAgentActionExecuteRecord {
operation: map_custom_world_agent_operation_snapshot(operation),
})
}
pub(crate) fn map_custom_world_library_entry_from_profile_snapshot(
snapshot: CustomWorldProfileSnapshot,
) -> Result<CustomWorldLibraryEntryRecord, SpacetimeClientError> {
let profile = serde_json::from_str::<serde_json::Value>(&snapshot.profile_payload_json)
.map_err(|error| {
SpacetimeClientError::Runtime(format!(
"custom world profile payload JSON 非法: {error}"
))
})?;
Ok(CustomWorldLibraryEntryRecord {
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
public_work_code: snapshot.public_work_code,
author_public_user_code: snapshot.author_public_user_code,
profile,
visibility: map_custom_world_publication_status(snapshot.publication_status).to_string(),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
author_display_name: snapshot.author_display_name,
world_name: snapshot.world_name,
subtitle: snapshot.subtitle,
summary_text: snapshot.summary_text,
cover_image_src: snapshot.cover_image_src,
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
snapshot.theme_mode,
))
.to_string(),
playable_npc_count: snapshot.playable_npc_count,
landmark_count: snapshot.landmark_count,
play_count: snapshot.play_count,
remix_count: snapshot.remix_count,
like_count: snapshot.like_count,
recent_play_count_7d: 0,
})
}
pub(crate) fn map_custom_world_gallery_entry_snapshot(
snapshot: CustomWorldGalleryEntrySnapshot,
) -> Result<CustomWorldGalleryEntryRecord, SpacetimeClientError> {
Ok(CustomWorldGalleryEntryRecord {
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
public_work_code: snapshot.public_work_code,
author_public_user_code: snapshot.author_public_user_code,
visibility: "published".to_string(),
published_at: Some(format_timestamp_micros(snapshot.published_at_micros)),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
author_display_name: snapshot.author_display_name,
world_name: snapshot.world_name,
subtitle: snapshot.subtitle,
summary_text: snapshot.summary_text,
cover_image_src: snapshot.cover_image_src,
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
snapshot.theme_mode,
))
.to_string(),
playable_npc_count: snapshot.playable_npc_count,
landmark_count: snapshot.landmark_count,
play_count: snapshot.play_count,
remix_count: snapshot.remix_count,
like_count: snapshot.like_count,
recent_play_count_7d: snapshot.recent_play_count_7_d,
})
}
pub(crate) fn map_custom_world_gallery_entry_row(
row: CustomWorldGalleryEntry,
recent_play_count_7d: u32,
) -> CustomWorldGalleryEntryRecord {
CustomWorldGalleryEntryRecord {
owner_user_id: row.owner_user_id,
profile_id: row.profile_id,
public_work_code: row.public_work_code,
author_public_user_code: row.author_public_user_code,
visibility: "published".to_string(),
published_at: Some(format_timestamp_micros(
row.published_at.to_micros_since_unix_epoch(),
)),
updated_at: format_timestamp_micros(row.updated_at.to_micros_since_unix_epoch()),
author_display_name: row.author_display_name,
world_name: row.world_name,
subtitle: row.subtitle,
summary_text: row.summary_text,
cover_image_src: row.cover_image_src,
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
row.theme_mode,
))
.to_string(),
playable_npc_count: row.playable_npc_count,
landmark_count: row.landmark_count,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
recent_play_count_7d,
}
}
pub(crate) fn map_custom_world_published_profile_compile_snapshot(
snapshot: CustomWorldPublishedProfileCompileSnapshot,
) -> Result<CustomWorldPublishedProfileCompileRecord, SpacetimeClientError> {
let compiled_profile =
serde_json::from_str::<serde_json::Value>(&snapshot.compiled_profile_payload_json)
.map_err(|error| {
SpacetimeClientError::Runtime(format!(
"published profile compile JSON 非法: {error}"
))
})?;
Ok(CustomWorldPublishedProfileCompileRecord {
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
world_name: snapshot.world_name,
subtitle: snapshot.subtitle,
summary_text: snapshot.summary_text,
theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back(
snapshot.theme_mode,
))
.to_string(),
cover_image_src: snapshot.cover_image_src,
playable_npc_count: snapshot.playable_npc_count,
landmark_count: snapshot.landmark_count,
author_display_name: snapshot.author_display_name,
compiled_profile: compiled_profile,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
pub(crate) fn map_custom_world_work_summary_snapshot(
snapshot: CustomWorldWorkSummarySnapshot,
) -> Result<CustomWorldWorkSummaryRecord, SpacetimeClientError> {
Ok(CustomWorldWorkSummaryRecord {
work_id: snapshot.work_id,
source_type: snapshot.source_type,
status: snapshot.status,
title: snapshot.title,
subtitle: snapshot.subtitle,
summary: snapshot.summary,
cover_image_src: snapshot.cover_image_src,
cover_render_mode: snapshot.cover_render_mode,
cover_character_image_srcs: parse_json_string_array(
&snapshot.cover_character_image_srcs_json,
"custom world work cover_character_image_srcs_json",
)?,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
stage: snapshot.stage.map(map_rpg_agent_stage),
stage_label: snapshot.stage_label,
playable_npc_count: snapshot.playable_npc_count,
landmark_count: snapshot.landmark_count,
role_visual_ready_count: snapshot.role_visual_ready_count,
role_animation_ready_count: snapshot.role_animation_ready_count,
role_asset_summary_label: snapshot.role_asset_summary_label,
session_id: snapshot.session_id,
profile_id: snapshot.profile_id,
can_resume: snapshot.can_resume,
can_enter_world: snapshot.can_enter_world,
blocker_count: snapshot.blocker_count,
publish_ready: snapshot.publish_ready,
})
}
pub(crate) fn map_custom_world_agent_session_snapshot(
snapshot: CustomWorldAgentSessionSnapshot,
) -> Result<CustomWorldAgentSessionRecord, SpacetimeClientError> {
let anchor_content = parse_json_value(
&snapshot.anchor_content_json,
"custom world agent anchor_content_json",
)?;
let creator_intent = parse_optional_json_value(
snapshot.creator_intent_json.as_deref(),
serde_json::json!({}),
"custom world agent creator_intent_json",
)?;
let creator_intent_readiness = parse_json_value(
&snapshot.creator_intent_readiness_json,
"custom world agent creator_intent_readiness_json",
)?;
let anchor_pack = parse_optional_json_value(
snapshot.anchor_pack_json.as_deref(),
serde_json::json!({}),
"custom world agent anchor_pack_json",
)?;
let lock_state = parse_optional_json_value(
snapshot.lock_state_json.as_deref(),
serde_json::json!({}),
"custom world agent lock_state_json",
)?;
let draft_profile = parse_optional_json_value(
snapshot.draft_profile_json.as_deref(),
serde_json::json!({}),
"custom world agent draft_profile_json",
)?;
let pending_clarifications = parse_json_array(
&snapshot.pending_clarifications_json,
"custom world agent pending_clarifications_json",
)?;
let suggested_actions = parse_json_array(
&snapshot.suggested_actions_json,
"custom world agent suggested_actions_json",
)?;
let recommended_replies = parse_json_string_array(
&snapshot.recommended_replies_json,
"custom world agent recommended_replies_json",
)?;
let quality_findings = parse_json_array(
&snapshot.quality_findings_json,
"custom world agent quality_findings_json",
)?;
let asset_coverage = parse_json_value(
&snapshot.asset_coverage_json,
"custom world agent asset_coverage_json",
)?;
let checkpoints_json = parse_json_array(
&snapshot.checkpoints_json,
"custom world agent checkpoints_json",
)?;
let checkpoints = checkpoints_json
.into_iter()
.map(map_custom_world_checkpoint_record)
.collect::<Result<Vec<_>, _>>()?;
let supported_actions = parse_supported_actions_json(&snapshot.supported_actions_json)?;
let publish_gate = snapshot
.publish_gate_json
.as_deref()
.map(parse_custom_world_publish_gate_record)
.transpose()?;
Ok(CustomWorldAgentSessionRecord {
session_id: snapshot.session_id,
seed_text: snapshot.seed_text,
current_turn: snapshot.current_turn,
anchor_content,
progress_percent: snapshot.progress_percent,
last_assistant_reply: snapshot.last_assistant_reply,
stage: map_rpg_agent_stage(snapshot.stage),
focus_card_id: snapshot.focus_card_id,
creator_intent,
creator_intent_readiness,
anchor_pack,
lock_state,
draft_profile,
messages: snapshot
.messages
.into_iter()
.map(map_custom_world_agent_message_snapshot)
.collect(),
draft_cards: snapshot
.draft_cards
.into_iter()
.map(map_custom_world_draft_card_snapshot)
.collect::<Result<Vec<_>, _>>()?,
pending_clarifications,
suggested_actions,
recommended_replies,
quality_findings,
asset_coverage,
checkpoints,
supported_actions,
publish_gate,
result_preview: snapshot
.result_preview_json
.as_deref()
.map(|value| parse_json_value(value, "custom world agent result_preview_json"))
.transpose()?,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
pub(crate) fn map_custom_world_agent_message_snapshot(
snapshot: CustomWorldAgentMessageSnapshot,
) -> CustomWorldAgentMessageRecord {
CustomWorldAgentMessageRecord {
message_id: snapshot.message_id,
role: format_rpg_agent_message_role(snapshot.role).to_string(),
kind: format_rpg_agent_message_kind(snapshot.kind).to_string(),
text: snapshot.text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
related_operation_id: snapshot.related_operation_id,
}
}
pub(crate) fn map_custom_world_agent_operation_snapshot(
snapshot: CustomWorldAgentOperationSnapshot,
) -> CustomWorldAgentOperationRecord {
CustomWorldAgentOperationRecord {
operation_id: snapshot.operation_id,
operation_type: format_rpg_agent_operation_type(snapshot.operation_type).to_string(),
status: format_rpg_agent_operation_status(snapshot.status).to_string(),
phase_label: snapshot.phase_label,
phase_detail: snapshot.phase_detail,
progress: snapshot.progress,
error_message: snapshot.error_message,
started_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_custom_world_draft_card_snapshot(
snapshot: CustomWorldDraftCardSnapshot,
) -> Result<CustomWorldDraftCardRecord, SpacetimeClientError> {
Ok(CustomWorldDraftCardRecord {
card_id: snapshot.card_id,
kind: format_rpg_agent_draft_card_kind(snapshot.kind).to_string(),
title: snapshot.title,
subtitle: snapshot.subtitle,
summary: snapshot.summary,
status: format_rpg_agent_draft_card_status(snapshot.status).to_string(),
linked_ids: parse_json_string_array(
&snapshot.linked_ids_json,
"custom world draft_card linked_ids_json",
)?,
warning_count: snapshot.warning_count,
asset_status: snapshot
.asset_status
.map(format_custom_world_role_asset_status_back),
asset_status_label: snapshot.asset_status_label,
detail_payload: snapshot
.detail_payload_json
.as_deref()
.map(|value| parse_json_value(value, "custom world draft_card detail_payload_json"))
.transpose()?,
})
}
pub(crate) fn map_custom_world_draft_card_detail_snapshot(
snapshot: CustomWorldDraftCardDetailSnapshot,
) -> Result<CustomWorldDraftCardDetailRecord, SpacetimeClientError> {
Ok(CustomWorldDraftCardDetailRecord {
card_id: snapshot.card_id,
kind: format_rpg_agent_draft_card_kind(snapshot.kind).to_string(),
title: snapshot.title,
sections: snapshot
.sections
.into_iter()
.map(map_custom_world_draft_card_detail_section_snapshot)
.collect(),
linked_ids: parse_json_string_array(
&snapshot.linked_ids_json,
"custom world card detail linked_ids_json",
)?,
locked: snapshot.locked,
editable: snapshot.editable,
editable_section_ids: parse_json_string_array(
&snapshot.editable_section_ids_json,
"custom world card detail editable_section_ids_json",
)?,
warning_messages: parse_json_string_array(
&snapshot.warning_messages_json,
"custom world card detail warning_messages_json",
)?,
asset_status: snapshot
.asset_status
.map(format_custom_world_role_asset_status_back),
asset_status_label: snapshot.asset_status_label,
})
}
pub(crate) fn map_custom_world_draft_card_detail_section_snapshot(
snapshot: CustomWorldDraftCardDetailSectionSnapshot,
) -> CustomWorldDraftCardDetailSectionRecord {
CustomWorldDraftCardDetailSectionRecord {
section_id: snapshot.section_id,
label: snapshot.label,
value: snapshot.value,
}
}
pub(crate) fn map_custom_world_theme_mode(
value: DomainCustomWorldThemeMode,
) -> CustomWorldThemeMode {
match value {
DomainCustomWorldThemeMode::Martial => CustomWorldThemeMode::Martial,
DomainCustomWorldThemeMode::Arcane => CustomWorldThemeMode::Arcane,
DomainCustomWorldThemeMode::Machina => CustomWorldThemeMode::Machina,
DomainCustomWorldThemeMode::Tide => CustomWorldThemeMode::Tide,
DomainCustomWorldThemeMode::Rift => CustomWorldThemeMode::Rift,
DomainCustomWorldThemeMode::Mythic => CustomWorldThemeMode::Mythic,
}
}
pub(crate) fn map_custom_world_theme_mode_back(
value: CustomWorldThemeMode,
) -> DomainCustomWorldThemeMode {
match value {
CustomWorldThemeMode::Martial => DomainCustomWorldThemeMode::Martial,
CustomWorldThemeMode::Arcane => DomainCustomWorldThemeMode::Arcane,
CustomWorldThemeMode::Machina => DomainCustomWorldThemeMode::Machina,
CustomWorldThemeMode::Tide => DomainCustomWorldThemeMode::Tide,
CustomWorldThemeMode::Rift => DomainCustomWorldThemeMode::Rift,
CustomWorldThemeMode::Mythic => DomainCustomWorldThemeMode::Mythic,
}
}
pub(crate) fn map_custom_world_publication_status(
value: CustomWorldPublicationStatus,
) -> &'static str {
match value {
CustomWorldPublicationStatus::Draft => "draft",
CustomWorldPublicationStatus::Published => "published",
}
}
pub(crate) fn map_rpg_agent_stage(value: crate::module_bindings::RpgAgentStage) -> String {
match value {
crate::module_bindings::RpgAgentStage::CollectingIntent => "collecting_intent",
crate::module_bindings::RpgAgentStage::Clarifying => "clarifying",
crate::module_bindings::RpgAgentStage::FoundationReview => "foundation_review",
crate::module_bindings::RpgAgentStage::ObjectRefining => "object_refining",
crate::module_bindings::RpgAgentStage::VisualRefining => "visual_refining",
crate::module_bindings::RpgAgentStage::LongTailReview => "long_tail_review",
crate::module_bindings::RpgAgentStage::ReadyToPublish => "ready_to_publish",
crate::module_bindings::RpgAgentStage::Published => "published",
crate::module_bindings::RpgAgentStage::Error => "error",
}
.to_string()
}
pub(crate) fn parse_rpg_agent_stage_record(
value: &str,
) -> Result<crate::module_bindings::RpgAgentStage, SpacetimeClientError> {
match value.trim() {
"collecting_intent" => Ok(crate::module_bindings::RpgAgentStage::CollectingIntent),
"clarifying" => Ok(crate::module_bindings::RpgAgentStage::Clarifying),
"foundation_review" => Ok(crate::module_bindings::RpgAgentStage::FoundationReview),
"object_refining" => Ok(crate::module_bindings::RpgAgentStage::ObjectRefining),
"visual_refining" => Ok(crate::module_bindings::RpgAgentStage::VisualRefining),
"long_tail_review" => Ok(crate::module_bindings::RpgAgentStage::LongTailReview),
"ready_to_publish" => Ok(crate::module_bindings::RpgAgentStage::ReadyToPublish),
"published" => Ok(crate::module_bindings::RpgAgentStage::Published),
"error" => Ok(crate::module_bindings::RpgAgentStage::Error),
other => Err(SpacetimeClientError::Runtime(format!(
"未知 rpg agent stage: {other}"
))),
}
}
pub(crate) fn format_rpg_agent_message_role(
value: crate::module_bindings::RpgAgentMessageRole,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentMessageRole::User => "user",
crate::module_bindings::RpgAgentMessageRole::Assistant => "assistant",
crate::module_bindings::RpgAgentMessageRole::System => "system",
}
}
pub(crate) fn format_rpg_agent_message_kind(
value: crate::module_bindings::RpgAgentMessageKind,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentMessageKind::Chat => "chat",
crate::module_bindings::RpgAgentMessageKind::Clarification => "clarification",
crate::module_bindings::RpgAgentMessageKind::Summary => "summary",
crate::module_bindings::RpgAgentMessageKind::Checkpoint => "checkpoint",
crate::module_bindings::RpgAgentMessageKind::Warning => "warning",
crate::module_bindings::RpgAgentMessageKind::ActionResult => "action_result",
}
}
pub(crate) fn format_rpg_agent_operation_type(
value: crate::module_bindings::RpgAgentOperationType,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentOperationType::ProcessMessage => "process_message",
crate::module_bindings::RpgAgentOperationType::DraftFoundation => "draft_foundation",
crate::module_bindings::RpgAgentOperationType::UpdateDraftCard => "update_draft_card",
crate::module_bindings::RpgAgentOperationType::SyncResultProfile => "sync_result_profile",
crate::module_bindings::RpgAgentOperationType::GenerateCharacters => "generate_characters",
crate::module_bindings::RpgAgentOperationType::GenerateLandmarks => "generate_landmarks",
crate::module_bindings::RpgAgentOperationType::GenerateRoleAssets => "generate_role_assets",
crate::module_bindings::RpgAgentOperationType::SyncRoleAssets => "sync_role_assets",
crate::module_bindings::RpgAgentOperationType::GenerateSceneAssets => {
"generate_scene_assets"
}
crate::module_bindings::RpgAgentOperationType::SyncSceneAssets => "sync_scene_assets",
crate::module_bindings::RpgAgentOperationType::ExpandLongTail => "expand_long_tail",
crate::module_bindings::RpgAgentOperationType::PublishWorld => "publish_world",
crate::module_bindings::RpgAgentOperationType::RevertCheckpoint => "revert_checkpoint",
crate::module_bindings::RpgAgentOperationType::DeleteCharacters => "delete_characters",
crate::module_bindings::RpgAgentOperationType::DeleteLandmarks => "delete_landmarks",
}
}
pub(crate) fn parse_rpg_agent_operation_type_record(
value: &str,
) -> Result<crate::module_bindings::RpgAgentOperationType, SpacetimeClientError> {
match value.trim() {
"process_message" => Ok(crate::module_bindings::RpgAgentOperationType::ProcessMessage),
"draft_foundation" => Ok(crate::module_bindings::RpgAgentOperationType::DraftFoundation),
"update_draft_card" => Ok(crate::module_bindings::RpgAgentOperationType::UpdateDraftCard),
"sync_result_profile" => {
Ok(crate::module_bindings::RpgAgentOperationType::SyncResultProfile)
}
"generate_characters" => {
Ok(crate::module_bindings::RpgAgentOperationType::GenerateCharacters)
}
"generate_landmarks" => {
Ok(crate::module_bindings::RpgAgentOperationType::GenerateLandmarks)
}
"generate_role_assets" => {
Ok(crate::module_bindings::RpgAgentOperationType::GenerateRoleAssets)
}
"sync_role_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncRoleAssets),
"generate_scene_assets" => {
Ok(crate::module_bindings::RpgAgentOperationType::GenerateSceneAssets)
}
"sync_scene_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncSceneAssets),
"expand_long_tail" => Ok(crate::module_bindings::RpgAgentOperationType::ExpandLongTail),
"publish_world" => Ok(crate::module_bindings::RpgAgentOperationType::PublishWorld),
"revert_checkpoint" => Ok(crate::module_bindings::RpgAgentOperationType::RevertCheckpoint),
"delete_characters" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteCharacters),
"delete_landmarks" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteLandmarks),
other => Err(SpacetimeClientError::Runtime(format!(
"未知 rpg agent operation type: {other}"
))),
}
}
pub(crate) fn format_rpg_agent_operation_status(
value: crate::module_bindings::RpgAgentOperationStatus,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentOperationStatus::Queued => "queued",
crate::module_bindings::RpgAgentOperationStatus::Running => "running",
crate::module_bindings::RpgAgentOperationStatus::Completed => "completed",
crate::module_bindings::RpgAgentOperationStatus::Failed => "failed",
}
}
pub(crate) fn parse_rpg_agent_operation_status_record(
value: &str,
) -> Result<crate::module_bindings::RpgAgentOperationStatus, SpacetimeClientError> {
match value.trim() {
"queued" => Ok(crate::module_bindings::RpgAgentOperationStatus::Queued),
"running" => Ok(crate::module_bindings::RpgAgentOperationStatus::Running),
"completed" => Ok(crate::module_bindings::RpgAgentOperationStatus::Completed),
"failed" => Ok(crate::module_bindings::RpgAgentOperationStatus::Failed),
other => Err(SpacetimeClientError::Runtime(format!(
"未知 rpg agent operation status: {other}"
))),
}
}
pub(crate) fn format_rpg_agent_draft_card_kind(
value: crate::module_bindings::RpgAgentDraftCardKind,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentDraftCardKind::World => "world",
crate::module_bindings::RpgAgentDraftCardKind::Camp => "camp",
crate::module_bindings::RpgAgentDraftCardKind::Faction => "faction",
crate::module_bindings::RpgAgentDraftCardKind::Character => "character",
crate::module_bindings::RpgAgentDraftCardKind::Landmark => "landmark",
crate::module_bindings::RpgAgentDraftCardKind::Thread => "thread",
crate::module_bindings::RpgAgentDraftCardKind::Chapter => "chapter",
crate::module_bindings::RpgAgentDraftCardKind::SceneChapter => "scene_chapter",
crate::module_bindings::RpgAgentDraftCardKind::Carrier => "carrier",
crate::module_bindings::RpgAgentDraftCardKind::SidequestSeed => "sidequest_seed",
}
}
pub(crate) fn format_rpg_agent_draft_card_status(
value: crate::module_bindings::RpgAgentDraftCardStatus,
) -> &'static str {
match value {
crate::module_bindings::RpgAgentDraftCardStatus::Suggested => "suggested",
crate::module_bindings::RpgAgentDraftCardStatus::Confirmed => "confirmed",
crate::module_bindings::RpgAgentDraftCardStatus::Locked => "locked",
crate::module_bindings::RpgAgentDraftCardStatus::Warning => "warning",
}
}
pub(crate) fn format_custom_world_role_asset_status_back(
value: crate::module_bindings::CustomWorldRoleAssetStatus,
) -> String {
match value {
crate::module_bindings::CustomWorldRoleAssetStatus::Missing => "missing",
crate::module_bindings::CustomWorldRoleAssetStatus::VisualReady => "visual_ready",
crate::module_bindings::CustomWorldRoleAssetStatus::AnimationsReady => "animations_ready",
crate::module_bindings::CustomWorldRoleAssetStatus::Complete => "complete",
}
.to_string()
}
pub(crate) fn format_custom_world_theme_mode(value: DomainCustomWorldThemeMode) -> &'static str {
match value {
DomainCustomWorldThemeMode::Martial => "martial",
DomainCustomWorldThemeMode::Arcane => "arcane",
DomainCustomWorldThemeMode::Machina => "machina",
DomainCustomWorldThemeMode::Tide => "tide",
DomainCustomWorldThemeMode::Rift => "rift",
DomainCustomWorldThemeMode::Mythic => "mythic",
}
}
pub(crate) fn format_ai_task_kind(value: AiTaskKind) -> &'static str {
match value {
AiTaskKind::StoryGeneration => "story_generation",
AiTaskKind::CharacterChat => "character_chat",
AiTaskKind::NpcChat => "npc_chat",
AiTaskKind::CustomWorldGeneration => "custom_world_generation",
AiTaskKind::QuestIntent => "quest_intent",
AiTaskKind::RuntimeItemIntent => "runtime_item_intent",
}
}
pub(crate) fn format_ai_result_reference_kind(value: AiResultReferenceKind) -> &'static str {
match value {
AiResultReferenceKind::StorySession => "story_session",
AiResultReferenceKind::StoryEvent => "story_event",
AiResultReferenceKind::CustomWorldProfile => "custom_world_profile",
AiResultReferenceKind::QuestRecord => "quest_record",
AiResultReferenceKind::RuntimeItemRecord => "runtime_item_record",
AiResultReferenceKind::AssetObject => "asset_object",
}
}
pub(crate) fn map_custom_world_checkpoint_record(
value: serde_json::Value,
) -> Result<CustomWorldCheckpointRecord, SpacetimeClientError> {
let object = value.as_object().ok_or_else(|| {
SpacetimeClientError::Runtime("custom world checkpoint 必须是 JSON object".to_string())
})?;
let checkpoint_id = object
.get("checkpointId")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world checkpoint.checkpointId 缺失".to_string())
})?;
let created_at = object
.get("createdAt")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world checkpoint.createdAt 缺失".to_string())
})?;
let label = object
.get("label")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world checkpoint.label 缺失".to_string())
})?;
Ok(CustomWorldCheckpointRecord {
checkpoint_id: checkpoint_id.to_string(),
created_at: created_at.to_string(),
label: label.to_string(),
})
}
pub(crate) fn parse_custom_world_publish_gate_record(
value: &str,
) -> Result<CustomWorldPublishGateRecord, SpacetimeClientError> {
let object = parse_json_value(value, "custom world publish_gate_json")?
.as_object()
.cloned()
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish_gate_json 必须是 JSON object".to_string(),
)
})?;
let profile_id = object
.get("profileId")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world publish_gate.profileId 缺失".to_string())
})?;
let blockers = object
.get("blockers")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world publish_gate.blockers 缺失".to_string())
})?
.iter()
.cloned()
.map(|entry| {
let object = entry.as_object().ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish gate blocker 必须是 JSON object".to_string(),
)
})?;
let id = object
.get("id")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish gate blocker.id 缺失".to_string(),
)
})?;
let code = object
.get("code")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish gate blocker.code 缺失".to_string(),
)
})?;
let message = object
.get("message")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish gate blocker.message 缺失".to_string(),
)
})?;
Ok(CustomWorldResultPreviewBlockerRecord {
id: id.to_string(),
code: code.to_string(),
message: message.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
let blocker_count = object
.get("blockerCount")
.and_then(serde_json::Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world publish_gate.blockerCount 缺失".to_string())
})?;
let publish_ready = object
.get("publishReady")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| {
SpacetimeClientError::Runtime("custom world publish_gate.publishReady 缺失".to_string())
})?;
let can_enter_world = object
.get("canEnterWorld")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world publish_gate.canEnterWorld 缺失".to_string(),
)
})?;
Ok(CustomWorldPublishGateRecord {
profile_id: profile_id.to_string(),
blockers,
blocker_count,
publish_ready,
can_enter_world,
})
}

View File

@@ -0,0 +1,200 @@
use super::*;
impl From<DomainRuntimeInventoryStateQueryInput> for RuntimeInventoryStateQueryInput {
fn from(input: DomainRuntimeInventoryStateQueryInput) -> Self {
Self {
runtime_session_id: input.runtime_session_id,
actor_user_id: input.actor_user_id,
}
}
}
pub(crate) fn map_runtime_inventory_state_procedure_result(
result: RuntimeInventoryStateProcedureResult,
) -> Result<RuntimeInventoryStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.snapshot
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime inventory state 快照"))?;
Ok(build_runtime_inventory_state_record(
map_runtime_inventory_state_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_inventory_state_snapshot(
snapshot: RuntimeInventoryStateSnapshot,
) -> DomainRuntimeInventoryStateSnapshot {
DomainRuntimeInventoryStateSnapshot {
runtime_session_id: snapshot.runtime_session_id,
actor_user_id: snapshot.actor_user_id,
backpack_items: snapshot
.backpack_items
.into_iter()
.map(map_inventory_slot_snapshot)
.collect(),
equipment_items: snapshot
.equipment_items
.into_iter()
.map(map_inventory_slot_snapshot)
.collect(),
}
}
pub(crate) fn map_inventory_slot_snapshot(
snapshot: InventorySlotSnapshot,
) -> module_inventory::InventorySlotSnapshot {
module_inventory::InventorySlotSnapshot {
slot_id: snapshot.slot_id,
runtime_session_id: snapshot.runtime_session_id,
story_session_id: snapshot.story_session_id,
actor_user_id: snapshot.actor_user_id,
container_kind: map_inventory_container_kind(snapshot.container_kind),
slot_key: snapshot.slot_key,
item_id: snapshot.item_id,
category: snapshot.category,
name: snapshot.name,
description: snapshot.description,
quantity: snapshot.quantity,
rarity: map_inventory_item_rarity(snapshot.rarity),
tags: snapshot.tags,
stackable: snapshot.stackable,
stack_key: snapshot.stack_key,
equipment_slot_id: snapshot.equipment_slot_id.map(map_inventory_equipment_slot),
source_kind: map_inventory_item_source_kind(snapshot.source_kind),
source_reference_id: snapshot.source_reference_id,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_item_reward_item_rarity(
value: DomainRuntimeItemRewardItemRarity,
) -> RuntimeItemRewardItemRarity {
match value {
DomainRuntimeItemRewardItemRarity::Common => RuntimeItemRewardItemRarity::Common,
DomainRuntimeItemRewardItemRarity::Uncommon => RuntimeItemRewardItemRarity::Uncommon,
DomainRuntimeItemRewardItemRarity::Rare => RuntimeItemRewardItemRarity::Rare,
DomainRuntimeItemRewardItemRarity::Epic => RuntimeItemRewardItemRarity::Epic,
DomainRuntimeItemRewardItemRarity::Legendary => RuntimeItemRewardItemRarity::Legendary,
}
}
pub(crate) fn map_runtime_item_equipment_slot(
value: DomainRuntimeItemEquipmentSlot,
) -> RuntimeItemEquipmentSlot {
match value {
DomainRuntimeItemEquipmentSlot::Weapon => RuntimeItemEquipmentSlot::Weapon,
DomainRuntimeItemEquipmentSlot::Armor => RuntimeItemEquipmentSlot::Armor,
DomainRuntimeItemEquipmentSlot::Relic => RuntimeItemEquipmentSlot::Relic,
}
}
pub(crate) fn map_runtime_item_reward_item_rarity_back(
value: RuntimeItemRewardItemRarity,
) -> DomainRuntimeItemRewardItemRarity {
match value {
RuntimeItemRewardItemRarity::Common => DomainRuntimeItemRewardItemRarity::Common,
RuntimeItemRewardItemRarity::Uncommon => DomainRuntimeItemRewardItemRarity::Uncommon,
RuntimeItemRewardItemRarity::Rare => DomainRuntimeItemRewardItemRarity::Rare,
RuntimeItemRewardItemRarity::Epic => DomainRuntimeItemRewardItemRarity::Epic,
RuntimeItemRewardItemRarity::Legendary => DomainRuntimeItemRewardItemRarity::Legendary,
}
}
pub(crate) fn map_runtime_item_equipment_slot_back(
value: RuntimeItemEquipmentSlot,
) -> DomainRuntimeItemEquipmentSlot {
match value {
RuntimeItemEquipmentSlot::Weapon => DomainRuntimeItemEquipmentSlot::Weapon,
RuntimeItemEquipmentSlot::Armor => DomainRuntimeItemEquipmentSlot::Armor,
RuntimeItemEquipmentSlot::Relic => DomainRuntimeItemEquipmentSlot::Relic,
}
}
pub(crate) fn map_ai_result_reference_kind(
value: DomainAiResultReferenceKind,
) -> AiResultReferenceKind {
match value {
DomainAiResultReferenceKind::StorySession => AiResultReferenceKind::StorySession,
DomainAiResultReferenceKind::StoryEvent => AiResultReferenceKind::StoryEvent,
DomainAiResultReferenceKind::CustomWorldProfile => {
AiResultReferenceKind::CustomWorldProfile
}
DomainAiResultReferenceKind::QuestRecord => AiResultReferenceKind::QuestRecord,
DomainAiResultReferenceKind::RuntimeItemRecord => AiResultReferenceKind::RuntimeItemRecord,
DomainAiResultReferenceKind::AssetObject => AiResultReferenceKind::AssetObject,
}
}
pub(crate) fn map_runtime_item_reward_item_snapshot(
snapshot: DomainRuntimeItemRewardItemSnapshot,
) -> RuntimeItemRewardItemSnapshot {
RuntimeItemRewardItemSnapshot {
item_id: snapshot.item_id,
category: snapshot.category,
item_name: snapshot.item_name,
description: snapshot.description,
quantity: snapshot.quantity,
rarity: map_runtime_item_reward_item_rarity(snapshot.rarity),
tags: snapshot.tags,
stackable: snapshot.stackable,
stack_key: snapshot.stack_key,
equipment_slot_id: snapshot
.equipment_slot_id
.map(map_runtime_item_equipment_slot),
}
}
pub(crate) fn map_runtime_item_reward_item_snapshot_back(
snapshot: RuntimeItemRewardItemSnapshot,
) -> DomainRuntimeItemRewardItemSnapshot {
DomainRuntimeItemRewardItemSnapshot {
item_id: snapshot.item_id,
category: snapshot.category,
item_name: snapshot.item_name,
description: snapshot.description,
quantity: snapshot.quantity,
rarity: map_runtime_item_reward_item_rarity_back(snapshot.rarity),
tags: snapshot.tags,
stackable: snapshot.stackable,
stack_key: snapshot.stack_key,
equipment_slot_id: snapshot
.equipment_slot_id
.map(map_runtime_item_equipment_slot_back),
}
}
pub(crate) fn map_inventory_container_kind(
value: InventoryContainerKind,
) -> module_inventory::InventoryContainerKind {
match value {
InventoryContainerKind::Backpack => module_inventory::InventoryContainerKind::Backpack,
InventoryContainerKind::Equipment => module_inventory::InventoryContainerKind::Equipment,
}
}
pub(crate) fn map_inventory_item_rarity(
value: InventoryItemRarity,
) -> module_inventory::InventoryItemRarity {
match value {
InventoryItemRarity::Common => module_inventory::InventoryItemRarity::Common,
InventoryItemRarity::Uncommon => module_inventory::InventoryItemRarity::Uncommon,
InventoryItemRarity::Rare => module_inventory::InventoryItemRarity::Rare,
InventoryItemRarity::Epic => module_inventory::InventoryItemRarity::Epic,
InventoryItemRarity::Legendary => module_inventory::InventoryItemRarity::Legendary,
}
}
pub(crate) fn map_inventory_equipment_slot(
value: InventoryEquipmentSlot,
) -> module_inventory::InventoryEquipmentSlot {
match value {
InventoryEquipmentSlot::Weapon => module_inventory::InventoryEquipmentSlot::Weapon,
InventoryEquipmentSlot::Armor => module_inventory::InventoryEquipmentSlot::Armor,
InventoryEquipmentSlot::Relic => module_inventory::InventoryEquipmentSlot::Relic,
}
}

View File

@@ -0,0 +1,606 @@
use super::*;
pub(crate) fn map_match3d_agent_session_procedure_result(
result: Match3DAgentSessionProcedureResult,
) -> Result<Match3DAgentSessionRecord, SpacetimeClientError> {
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<Match3DWorkProfileRecord, SpacetimeClientError> {
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<Vec<Match3DWorkProfileRecord>, 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<Match3DRunRecord, SpacetimeClientError> {
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<Match3DClickConfirmationRecord, SpacetimeClientError> {
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<String>,
) -> 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::<Vec<_>>();
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<u32>,
) -> 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<String>,
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<String>,
pub assistant_reply_text: Option<String>,
pub config_json: Option<String>,
pub progress_percent: u32,
pub stage: String,
pub updated_at_micros: i64,
pub error_message: Option<String>,
}
#[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<String>,
pub summary_text: Option<String>,
pub tags_json: Option<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub compiled_at_micros: i64,
pub generated_item_assets_json: Option<String>,
}
#[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<String>,
pub clear_count: u32,
pub difficulty: u32,
pub asset_style_id: Option<String>,
pub asset_style_label: Option<String>,
pub asset_style_prompt: Option<String>,
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<String>,
pub cover_image_src: Option<String>,
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub generated_item_assets_json: Option<String>,
pub total_item_count: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[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<Match3DCreatorConfigRecord>,
pub draft: Option<Match3DResultDraftRecord>,
pub messages: Vec<Match3DAgentMessageRecord>,
pub last_assistant_reply: Option<String>,
pub published_profile_id: Option<String>,
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<String>,
pub author_display_name: String,
pub game_name: String,
pub theme_text: String,
pub summary: String,
pub tags: Vec<String>,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub publication_status: String,
pub play_count: u32,
pub updated_at: String,
pub published_at: Option<String>,
pub publish_ready: bool,
pub generated_item_assets_json: Option<String>,
}
#[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<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match3DTraySlotRecord {
pub slot_index: u32,
pub item_instance_id: Option<String>,
pub item_type_id: Option<String>,
pub visual_key: Option<String>,
}
#[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<u64>,
pub remaining_ms: u64,
pub clear_count: u32,
pub total_item_count: u32,
pub cleared_item_count: u32,
pub items: Vec<Match3DItemSnapshotRecord>,
pub tray_slots: Vec<Match3DTraySlotRecord>,
pub failure_reason: Option<String>,
pub last_confirmed_action_id: Option<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Match3DClickConfirmationRecord {
pub status: String,
pub accepted: bool,
pub reject_reason: Option<String>,
pub accepted_item_instance_id: Option<String>,
pub entered_slot_index: Option<u32>,
pub cleared_item_instance_ids: Vec<String>,
pub failure_reason: Option<String>,
pub run: Match3DRunRecord,
}

View File

@@ -0,0 +1,624 @@
use super::*;
impl From<DomainBattleStateInput> for BattleStateInput {
fn from(input: DomainBattleStateInput) -> Self {
Self {
battle_state_id: input.battle_state_id,
story_session_id: input.story_session_id,
runtime_session_id: input.runtime_session_id,
actor_user_id: input.actor_user_id,
chapter_id: input.chapter_id,
target_npc_id: input.target_npc_id,
target_name: input.target_name,
battle_mode: map_battle_mode(input.battle_mode),
player_hp: input.player_hp,
player_max_hp: input.player_max_hp,
player_mana: input.player_mana,
player_max_mana: input.player_max_mana,
target_hp: input.target_hp,
target_max_hp: input.target_max_hp,
experience_reward: input.experience_reward,
reward_items: input
.reward_items
.into_iter()
.map(map_runtime_item_reward_item_snapshot)
.collect(),
created_at_micros: input.created_at_micros,
}
}
}
pub(crate) fn map_npc_battle_interaction_procedure_result(
result: NpcBattleInteractionProcedureResult,
) -> Result<NpcBattleInteractionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let interaction_result = result
.result
.ok_or_else(|| SpacetimeClientError::missing_snapshot("NPC 开战结果"))?;
Ok(build_npc_battle_interaction_record(
map_npc_battle_interaction_result(interaction_result),
))
}
pub(crate) fn map_battle_state_snapshot(
snapshot: BattleStateSnapshot,
) -> DomainBattleStateSnapshot {
DomainBattleStateSnapshot {
battle_state_id: snapshot.battle_state_id,
story_session_id: snapshot.story_session_id,
runtime_session_id: snapshot.runtime_session_id,
actor_user_id: snapshot.actor_user_id,
chapter_id: snapshot.chapter_id,
target_npc_id: snapshot.target_npc_id,
target_name: snapshot.target_name,
battle_mode: map_battle_mode_back(snapshot.battle_mode),
status: map_battle_status(snapshot.status),
player_hp: snapshot.player_hp,
player_max_hp: snapshot.player_max_hp,
player_mana: snapshot.player_mana,
player_max_mana: snapshot.player_max_mana,
target_hp: snapshot.target_hp,
target_max_hp: snapshot.target_max_hp,
experience_reward: snapshot.experience_reward,
reward_items: snapshot
.reward_items
.into_iter()
.map(map_runtime_item_reward_item_snapshot_back)
.collect(),
turn_index: snapshot.turn_index,
last_action_function_id: snapshot.last_action_function_id,
last_action_text: snapshot.last_action_text,
last_result_text: snapshot.last_result_text,
last_damage_dealt: snapshot.last_damage_dealt,
last_damage_taken: snapshot.last_damage_taken,
last_outcome: map_combat_outcome(snapshot.last_outcome),
version: snapshot.version,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_npc_battle_interaction_result(
result: NpcBattleInteractionResult,
) -> NpcBattleInteractionSnapshot {
NpcBattleInteractionSnapshot {
interaction: map_npc_interaction_result(result.interaction),
battle_state: map_battle_state_snapshot(result.battle_state),
}
}
pub(crate) fn map_npc_interaction_result(
result: NpcInteractionResult,
) -> DomainNpcInteractionResult {
DomainNpcInteractionResult {
npc_state: map_npc_state_snapshot(result.npc_state),
interaction_status: map_npc_interaction_status(result.interaction_status),
action_text: result.action_text,
result_text: result.result_text,
story_text: result.story_text,
battle_mode: result.battle_mode.map(map_npc_interaction_battle_mode),
encounter_closed: result.encounter_closed,
affinity_changed: result.affinity_changed,
previous_affinity: result.previous_affinity,
next_affinity: result.next_affinity,
}
}
pub(crate) fn map_npc_state_snapshot(snapshot: NpcStateSnapshot) -> DomainNpcStateSnapshot {
DomainNpcStateSnapshot {
npc_state_id: snapshot.npc_state_id,
runtime_session_id: snapshot.runtime_session_id,
npc_id: snapshot.npc_id,
npc_name: snapshot.npc_name,
affinity: snapshot.affinity,
relation_state: map_npc_relation_state(snapshot.relation_state),
help_used: snapshot.help_used,
chatted_count: snapshot.chatted_count,
gifts_given: snapshot.gifts_given,
recruited: snapshot.recruited,
trade_stock_signature: snapshot.trade_stock_signature,
revealed_facts: snapshot.revealed_facts,
known_attribute_rumors: snapshot.known_attribute_rumors,
first_meaningful_contact_resolved: snapshot.first_meaningful_contact_resolved,
seen_backstory_chapter_ids: snapshot.seen_backstory_chapter_ids,
stance_profile: map_npc_stance_profile(snapshot.stance_profile),
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_npc_relation_state(value: NpcRelationState) -> DomainNpcRelationState {
DomainNpcRelationState {
affinity: value.affinity,
stance: map_npc_relation_stance(value.stance),
}
}
pub(crate) fn map_npc_stance_profile(value: NpcStanceProfile) -> DomainNpcStanceProfile {
DomainNpcStanceProfile {
trust: value.trust,
warmth: value.warmth,
ideological_fit: value.ideological_fit,
fear_or_guard: value.fear_or_guard,
loyalty: value.loyalty,
current_conflict_tag: value.current_conflict_tag,
recent_approvals: value.recent_approvals,
recent_disapprovals: value.recent_disapprovals,
}
}
pub(crate) fn map_npc_interaction_status(
value: NpcInteractionStatus,
) -> DomainNpcInteractionStatus {
match value {
NpcInteractionStatus::Previewed => DomainNpcInteractionStatus::Previewed,
NpcInteractionStatus::Dialogue => DomainNpcInteractionStatus::Dialogue,
NpcInteractionStatus::Resolved => DomainNpcInteractionStatus::Resolved,
NpcInteractionStatus::Recruited => DomainNpcInteractionStatus::Recruited,
NpcInteractionStatus::BattlePending => DomainNpcInteractionStatus::BattlePending,
NpcInteractionStatus::Left => DomainNpcInteractionStatus::Left,
}
}
pub(crate) fn map_npc_interaction_battle_mode(
value: NpcInteractionBattleMode,
) -> DomainNpcInteractionBattleMode {
match value {
NpcInteractionBattleMode::Fight => DomainNpcInteractionBattleMode::Fight,
NpcInteractionBattleMode::Spar => DomainNpcInteractionBattleMode::Spar,
}
}
pub(crate) fn map_npc_relation_stance(value: NpcRelationStance) -> DomainNpcRelationStance {
match value {
NpcRelationStance::Hostile => DomainNpcRelationStance::Hostile,
NpcRelationStance::Guarded => DomainNpcRelationStance::Guarded,
NpcRelationStance::Neutral => DomainNpcRelationStance::Neutral,
NpcRelationStance::Cooperative => DomainNpcRelationStance::Cooperative,
NpcRelationStance::Bonded => DomainNpcRelationStance::Bonded,
}
}
pub(crate) fn map_ai_task_kind(value: DomainAiTaskKind) -> AiTaskKind {
match value {
DomainAiTaskKind::StoryGeneration => AiTaskKind::StoryGeneration,
DomainAiTaskKind::CharacterChat => AiTaskKind::CharacterChat,
DomainAiTaskKind::NpcChat => AiTaskKind::NpcChat,
DomainAiTaskKind::CustomWorldGeneration => AiTaskKind::CustomWorldGeneration,
DomainAiTaskKind::QuestIntent => AiTaskKind::QuestIntent,
DomainAiTaskKind::RuntimeItemIntent => AiTaskKind::RuntimeItemIntent,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BattleStateRecord {
pub battle_state_id: String,
pub story_session_id: String,
pub runtime_session_id: String,
pub actor_user_id: String,
pub chapter_id: Option<String>,
pub target_npc_id: String,
pub target_name: String,
pub battle_mode: String,
pub status: String,
pub player_hp: i32,
pub player_max_hp: i32,
pub player_mana: i32,
pub player_max_mana: i32,
pub target_hp: i32,
pub target_max_hp: i32,
pub experience_reward: u32,
pub reward_items: Vec<DomainRuntimeItemRewardItemSnapshot>,
pub turn_index: u32,
pub last_action_function_id: Option<String>,
pub last_action_text: Option<String>,
pub last_result_text: Option<String>,
pub last_damage_dealt: i32,
pub last_damage_taken: i32,
pub last_outcome: String,
pub version: u32,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldLibraryEntryRecord {
pub owner_user_id: String,
pub profile_id: String,
pub public_work_code: Option<String>,
pub author_public_user_code: Option<String>,
pub profile: serde_json::Value,
pub visibility: String,
pub published_at: Option<String>,
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<String>,
pub theme_mode: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub play_count: u32,
pub remix_count: u32,
pub like_count: u32,
pub recent_play_count_7d: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldGalleryEntryRecord {
pub owner_user_id: String,
pub profile_id: String,
pub public_work_code: String,
pub author_public_user_code: String,
pub visibility: String,
pub published_at: Option<String>,
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<String>,
pub theme_mode: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub play_count: u32,
pub remix_count: u32,
pub like_count: u32,
pub recent_play_count_7d: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CustomWorldPublishedProfileCompileRecord {
pub profile_id: String,
pub owner_user_id: String,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub theme_mode: String,
pub cover_image_src: Option<String>,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub author_display_name: String,
pub compiled_profile: serde_json::Value,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldWorkSummaryRecord {
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<String>,
pub cover_render_mode: Option<String>,
pub cover_character_image_srcs: Vec<String>,
pub updated_at: String,
pub published_at: Option<String>,
pub stage: Option<String>,
pub stage_label: Option<String>,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub role_visual_ready_count: Option<u32>,
pub role_animation_ready_count: Option<u32>,
pub role_asset_summary_label: Option<String>,
pub session_id: Option<String>,
pub profile_id: Option<String>,
pub can_resume: bool,
pub can_enter_world: bool,
pub blocker_count: u32,
pub publish_ready: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CustomWorldProfileUpsertRecordInput {
pub profile_id: String,
pub owner_user_id: String,
pub public_work_code: Option<String>,
pub author_public_user_code: Option<String>,
pub source_agent_session_id: Option<String>,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub theme_mode: DomainCustomWorldThemeMode,
pub cover_image_src: Option<String>,
pub profile_payload_json: String,
pub playable_npc_count: u32,
pub landmark_count: u32,
pub author_display_name: String,
pub updated_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolveNpcBattleInteractionInput {
pub npc_interaction: DomainResolveNpcInteractionInput,
pub story_session_id: String,
pub actor_user_id: String,
pub battle_state_id: Option<String>,
pub player_hp: i32,
pub player_max_hp: i32,
pub player_mana: i32,
pub player_max_mana: i32,
pub target_hp: i32,
pub target_max_hp: i32,
pub experience_reward: u32,
pub reward_items: Vec<DomainRuntimeItemRewardItemSnapshot>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NpcStateRecord {
pub npc_state_id: String,
pub runtime_session_id: String,
pub npc_id: String,
pub npc_name: String,
pub affinity: i32,
pub relation_stance: String,
pub help_used: bool,
pub chatted_count: u32,
pub gifts_given: u32,
pub recruited: bool,
pub trade_stock_signature: Option<String>,
pub revealed_facts: Vec<String>,
pub known_attribute_rumors: Vec<String>,
pub first_meaningful_contact_resolved: bool,
pub seen_backstory_chapter_ids: Vec<String>,
pub trust: u8,
pub warmth: u8,
pub ideological_fit: u8,
pub fear_or_guard: u8,
pub loyalty: u8,
pub current_conflict_tag: Option<String>,
pub recent_approvals: Vec<String>,
pub recent_disapprovals: Vec<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NpcInteractionRecord {
pub npc_state: NpcStateRecord,
pub interaction_status: String,
pub action_text: String,
pub result_text: String,
pub story_text: Option<String>,
pub battle_mode: Option<String>,
pub encounter_closed: bool,
pub affinity_changed: bool,
pub previous_affinity: i32,
pub next_affinity: i32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NpcBattleInteractionRecord {
pub npc_interaction: NpcInteractionRecord,
pub battle_state: BattleStateRecord,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct NpcBattleInteractionSnapshot {
interaction: DomainNpcInteractionResult,
battle_state: DomainBattleStateSnapshot,
}
pub(crate) fn build_battle_state_record(snapshot: DomainBattleStateSnapshot) -> BattleStateRecord {
BattleStateRecord {
battle_state_id: snapshot.battle_state_id,
story_session_id: snapshot.story_session_id,
runtime_session_id: snapshot.runtime_session_id,
actor_user_id: snapshot.actor_user_id,
chapter_id: snapshot.chapter_id,
target_npc_id: snapshot.target_npc_id,
target_name: snapshot.target_name,
battle_mode: snapshot.battle_mode.as_str().to_string(),
status: snapshot.status.as_str().to_string(),
player_hp: snapshot.player_hp,
player_max_hp: snapshot.player_max_hp,
player_mana: snapshot.player_mana,
player_max_mana: snapshot.player_max_mana,
target_hp: snapshot.target_hp,
target_max_hp: snapshot.target_max_hp,
experience_reward: snapshot.experience_reward,
reward_items: snapshot.reward_items,
turn_index: snapshot.turn_index,
last_action_function_id: snapshot.last_action_function_id,
last_action_text: snapshot.last_action_text,
last_result_text: snapshot.last_result_text,
last_damage_dealt: snapshot.last_damage_dealt,
last_damage_taken: snapshot.last_damage_taken,
last_outcome: snapshot.last_outcome.as_str().to_string(),
version: snapshot.version,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
impl From<ResolveNpcBattleInteractionInput>
for crate::module_bindings::ResolveNpcBattleInteractionInput
{
fn from(input: ResolveNpcBattleInteractionInput) -> Self {
Self {
npc_interaction: crate::module_bindings::ResolveNpcInteractionInput {
runtime_session_id: input.npc_interaction.runtime_session_id,
npc_id: input.npc_interaction.npc_id,
npc_name: input.npc_interaction.npc_name,
interaction_function_id: input.npc_interaction.interaction_function_id,
release_npc_id: input.npc_interaction.release_npc_id,
updated_at_micros: input.npc_interaction.updated_at_micros,
},
story_session_id: input.story_session_id,
actor_user_id: input.actor_user_id,
battle_state_id: input.battle_state_id,
player_hp: input.player_hp,
player_max_hp: input.player_max_hp,
player_mana: input.player_mana,
player_max_mana: input.player_max_mana,
target_hp: input.target_hp,
target_max_hp: input.target_max_hp,
experience_reward: input.experience_reward,
reward_items: input
.reward_items
.into_iter()
.map(map_runtime_item_reward_item_snapshot)
.collect(),
}
}
}
pub(crate) fn validate_npc_battle_interaction_input(
input: &ResolveNpcBattleInteractionInput,
) -> Result<(), SpacetimeClientError> {
let battle_state_input = DomainBattleStateInput {
battle_state_id: input
.battle_state_id
.clone()
.unwrap_or_else(|| "battle_preview".to_string()),
story_session_id: input.story_session_id.clone(),
runtime_session_id: input.npc_interaction.runtime_session_id.clone(),
actor_user_id: input.actor_user_id.clone(),
chapter_id: None,
target_npc_id: input.npc_interaction.npc_id.clone(),
target_name: input.npc_interaction.npc_name.clone(),
battle_mode: DomainBattleMode::Fight,
player_hp: input.player_hp,
player_max_hp: input.player_max_hp,
player_mana: input.player_mana,
player_max_mana: input.player_max_mana,
target_hp: input.target_hp,
target_max_hp: input.target_max_hp,
experience_reward: input.experience_reward,
reward_items: input.reward_items.clone(),
created_at_micros: input.npc_interaction.updated_at_micros,
};
validate_battle_state_input(&battle_state_input)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
for reward_item in input.reward_items.iter().cloned() {
normalize_reward_item_snapshot(reward_item)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?;
}
Ok(())
}
pub(crate) fn build_npc_state_record(snapshot: DomainNpcStateSnapshot) -> NpcStateRecord {
NpcStateRecord {
npc_state_id: snapshot.npc_state_id,
runtime_session_id: snapshot.runtime_session_id,
npc_id: snapshot.npc_id,
npc_name: snapshot.npc_name,
affinity: snapshot.affinity,
relation_stance: format_npc_relation_stance(snapshot.relation_state.stance).to_string(),
help_used: snapshot.help_used,
chatted_count: snapshot.chatted_count,
gifts_given: snapshot.gifts_given,
recruited: snapshot.recruited,
trade_stock_signature: snapshot.trade_stock_signature,
revealed_facts: snapshot.revealed_facts,
known_attribute_rumors: snapshot.known_attribute_rumors,
first_meaningful_contact_resolved: snapshot.first_meaningful_contact_resolved,
seen_backstory_chapter_ids: snapshot.seen_backstory_chapter_ids,
trust: snapshot.stance_profile.trust,
warmth: snapshot.stance_profile.warmth,
ideological_fit: snapshot.stance_profile.ideological_fit,
fear_or_guard: snapshot.stance_profile.fear_or_guard,
loyalty: snapshot.stance_profile.loyalty,
current_conflict_tag: snapshot.stance_profile.current_conflict_tag,
recent_approvals: snapshot.stance_profile.recent_approvals,
recent_disapprovals: snapshot.stance_profile.recent_disapprovals,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn build_npc_interaction_record(
result: DomainNpcInteractionResult,
) -> NpcInteractionRecord {
NpcInteractionRecord {
npc_state: build_npc_state_record(result.npc_state),
interaction_status: format_npc_interaction_status(result.interaction_status).to_string(),
action_text: result.action_text,
result_text: result.result_text,
story_text: result.story_text,
battle_mode: result
.battle_mode
.map(|mode| format_npc_interaction_battle_mode(mode).to_string()),
encounter_closed: result.encounter_closed,
affinity_changed: result.affinity_changed,
previous_affinity: result.previous_affinity,
next_affinity: result.next_affinity,
}
}
pub(crate) fn build_npc_battle_interaction_record(
result: NpcBattleInteractionSnapshot,
) -> NpcBattleInteractionRecord {
NpcBattleInteractionRecord {
npc_interaction: build_npc_interaction_record(result.interaction),
battle_state: build_battle_state_record(result.battle_state),
}
}
pub(crate) fn format_npc_relation_stance(value: DomainNpcRelationStance) -> &'static str {
match value {
DomainNpcRelationStance::Hostile => "hostile",
DomainNpcRelationStance::Guarded => "guarded",
DomainNpcRelationStance::Neutral => "neutral",
DomainNpcRelationStance::Cooperative => "cooperative",
DomainNpcRelationStance::Bonded => "bonded",
}
}
pub(crate) fn format_npc_interaction_status(value: DomainNpcInteractionStatus) -> &'static str {
match value {
DomainNpcInteractionStatus::Previewed => "previewed",
DomainNpcInteractionStatus::Dialogue => "dialogue",
DomainNpcInteractionStatus::Resolved => "resolved",
DomainNpcInteractionStatus::Recruited => "recruited",
DomainNpcInteractionStatus::BattlePending => "battle_pending",
DomainNpcInteractionStatus::Left => "left",
}
}
pub(crate) fn format_npc_interaction_battle_mode(
value: DomainNpcInteractionBattleMode,
) -> &'static str {
match value {
DomainNpcInteractionBattleMode::Fight => "fight",
DomainNpcInteractionBattleMode::Spar => "spar",
}
}
pub(crate) fn map_inventory_item_source_kind(
value: InventoryItemSourceKind,
) -> module_inventory::InventoryItemSourceKind {
match value {
InventoryItemSourceKind::StoryReward => {
module_inventory::InventoryItemSourceKind::StoryReward
}
InventoryItemSourceKind::QuestReward => {
module_inventory::InventoryItemSourceKind::QuestReward
}
InventoryItemSourceKind::TreasureReward => {
module_inventory::InventoryItemSourceKind::TreasureReward
}
InventoryItemSourceKind::NpcGift => module_inventory::InventoryItemSourceKind::NpcGift,
InventoryItemSourceKind::NpcTrade => module_inventory::InventoryItemSourceKind::NpcTrade,
InventoryItemSourceKind::CombatDrop => {
module_inventory::InventoryItemSourceKind::CombatDrop
}
InventoryItemSourceKind::ForgeCraft => {
module_inventory::InventoryItemSourceKind::ForgeCraft
}
InventoryItemSourceKind::ForgeReforge => {
module_inventory::InventoryItemSourceKind::ForgeReforge
}
InventoryItemSourceKind::ManualPatch => {
module_inventory::InventoryItemSourceKind::ManualPatch
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,440 @@
use super::*;
impl From<module_runtime::CreationEntryTypeAdminUpsertInput> 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,
}
}
}
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeSettingUpsertInput> 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<module_runtime::RuntimeBrowseHistoryListInput> for RuntimeBrowseHistoryListInput {
fn from(input: module_runtime::RuntimeBrowseHistoryListInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeBrowseHistoryClearInput> for RuntimeBrowseHistoryClearInput {
fn from(input: module_runtime::RuntimeBrowseHistoryClearInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeBrowseHistorySyncInput> 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<module_runtime::RuntimeBrowseHistoryWriteInput> 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<module_runtime::RuntimeSnapshotGetInput> for RuntimeSnapshotGetInput {
fn from(input: module_runtime::RuntimeSnapshotGetInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeSnapshotDeleteInput> for RuntimeSnapshotDeleteInput {
fn from(input: module_runtime::RuntimeSnapshotDeleteInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
pub type CreationEntryConfigRecord =
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
pub(crate) fn map_creation_entry_config_procedure_result(
result: CreationEntryConfigProcedureResult,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
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 build_creation_entry_config_record_from_rows(
header: CreationEntryConfig,
mut creation_types: Vec<CreationEntryTypeConfig>,
) -> 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,
},
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,
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
})
.collect(),
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
},
)
}
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,
},
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,
updated_at_micros: item.updated_at_micros,
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_setting_procedure_result(
result: RuntimeSettingProcedureResult,
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {
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_snapshot_procedure_result(
result: RuntimeSnapshotProcedureResult,
) -> Result<Option<RuntimeSnapshotRecord>, 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<RuntimeSnapshotRecord, SpacetimeClientError> {
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<bool, SpacetimeClientError> {
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::Value, SpacetimeClientError> {
serde_json::from_str::<serde_json::Value>(value)
.map_err(|error| SpacetimeClientError::Runtime(format!("{label} 非法: {error}")))
}
pub(crate) fn parse_json_array(
value: &str,
label: &str,
) -> Result<Vec<serde_json::Value>, 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<Vec<String>, 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<Vec<CustomWorldSupportedActionRecord>, 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<u32>,
pub threat_spawn_delta_levels: Vec<u32>,
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<BigFishLevelBlueprintRecord>,
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<String>,
pub owned_entities: Vec<BigFishRuntimeEntityRecord>,
pub wild_entities: Vec<BigFishRuntimeEntityRecord>,
pub camera_center: BigFishVector2Record,
pub last_input: BigFishVector2Record,
pub event_log: Vec<String>,
pub updated_at: String,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,417 @@
use super::*;
pub(crate) fn map_square_hole_agent_session_procedure_result(
result: SquareHoleAgentSessionProcedureResult,
) -> Result<SquareHoleAgentSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole agent session 快照"))?;
Ok(map_square_hole_agent_session_snapshot(session))
}
pub(crate) fn map_square_hole_work_procedure_result(
result: SquareHoleWorkProcedureResult,
) -> Result<SquareHoleWorkProfileRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let work = result
.work
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole work 快照"))?;
Ok(map_square_hole_work_snapshot(work))
}
pub(crate) fn map_square_hole_works_procedure_result(
result: SquareHoleWorksProcedureResult,
) -> Result<Vec<SquareHoleWorkProfileRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.items
.into_iter()
.map(map_square_hole_work_snapshot)
.collect())
}
pub(crate) fn map_square_hole_run_procedure_result(
result: SquareHoleRunProcedureResult,
) -> Result<SquareHoleRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole run 快照"))?;
Ok(map_square_hole_run_snapshot(run))
}
pub(crate) fn map_square_hole_drop_shape_procedure_result(
result: SquareHoleDropShapeProcedureResult,
) -> Result<SquareHoleDropConfirmationRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop run 快照"))?;
let feedback = result
.feedback
.ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop feedback 快照"))?;
let run = map_square_hole_run_snapshot(run);
Ok(SquareHoleDropConfirmationRecord {
status: result.status,
accepted: feedback.accepted,
reject_reason: feedback.reject_reason.clone(),
failure_reason: result.failure_reason,
feedback: map_square_hole_feedback_snapshot(feedback),
run,
})
}
fn map_square_hole_agent_session_snapshot(
snapshot: SquareHoleAgentSessionSnapshot,
) -> SquareHoleAgentSessionRecord {
let config = map_square_hole_creator_config(snapshot.config);
SquareHoleAgentSessionRecord {
session_id: snapshot.session_id,
current_turn: snapshot.current_turn,
progress_percent: snapshot.progress_percent,
stage: normalize_square_hole_stage(&snapshot.stage).to_string(),
anchor_pack: build_square_hole_anchor_pack(&config),
config,
draft: snapshot.draft.map(map_square_hole_result_draft),
messages: snapshot
.messages
.into_iter()
.map(map_square_hole_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_square_hole_creator_config(
snapshot: SquareHoleCreatorConfigSnapshot,
) -> SquareHoleCreatorConfigRecord {
SquareHoleCreatorConfigRecord {
theme_text: snapshot.theme_text,
twist_rule: snapshot.twist_rule,
shape_count: snapshot.shape_count,
difficulty: snapshot.difficulty,
shape_options: snapshot
.shape_options
.into_iter()
.map(map_square_hole_shape_option)
.collect(),
hole_options: snapshot
.hole_options
.into_iter()
.map(map_square_hole_hole_option)
.collect(),
background_prompt: snapshot.background_prompt,
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
background_image_src: empty_string_to_none(snapshot.background_image_src),
}
}
fn map_square_hole_result_draft(snapshot: SquareHoleDraftSnapshot) -> SquareHoleResultDraftRecord {
SquareHoleResultDraftRecord {
profile_id: snapshot.profile_id,
game_name: snapshot.game_name,
theme_text: snapshot.theme_text,
twist_rule: snapshot.twist_rule,
summary: snapshot.summary_text,
tags: snapshot.tags,
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
background_prompt: snapshot.background_prompt,
background_image_src: empty_string_to_none(snapshot.background_image_src),
shape_options: snapshot
.shape_options
.into_iter()
.map(map_square_hole_shape_option)
.collect(),
hole_options: snapshot
.hole_options
.into_iter()
.map(map_square_hole_hole_option)
.collect(),
shape_count: snapshot.shape_count,
difficulty: snapshot.difficulty,
publish_ready: false,
blockers: Vec::new(),
}
}
fn map_square_hole_agent_message_snapshot(
snapshot: SquareHoleAgentMessageSnapshot,
) -> SquareHoleAgentMessageRecord {
SquareHoleAgentMessageRecord {
id: snapshot.message_id,
role: snapshot.role,
kind: normalize_square_hole_message_kind(&snapshot.kind).to_string(),
text: snapshot.text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_square_hole_work_snapshot(snapshot: SquareHoleWorkSnapshot) -> SquareHoleWorkProfileRecord {
SquareHoleWorkProfileRecord {
work_id: snapshot.work_id,
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,
twist_rule: snapshot.twist_rule,
summary: snapshot.summary_text,
tags: snapshot.tags,
cover_image_src: empty_string_to_none(snapshot.cover_image_src),
background_prompt: snapshot.background_prompt,
background_image_src: empty_string_to_none(snapshot.background_image_src),
shape_options: snapshot
.shape_options
.into_iter()
.map(map_square_hole_shape_option)
.collect(),
hole_options: snapshot
.hole_options
.into_iter()
.map(map_square_hole_hole_option)
.collect(),
shape_count: snapshot.shape_count,
difficulty: snapshot.difficulty,
publication_status: normalize_square_hole_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,
}
}
pub(crate) fn map_square_hole_gallery_view_row(
row: SquareHoleGalleryViewRow,
) -> SquareHoleWorkProfileRecord {
SquareHoleWorkProfileRecord {
work_id: row.work_id,
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,
twist_rule: row.twist_rule,
summary: row.summary_text,
tags: row.tags,
cover_image_src: empty_string_to_none(row.cover_image_src),
background_prompt: row.background_prompt,
background_image_src: empty_string_to_none(row.background_image_src),
shape_options: row
.shape_options
.into_iter()
.map(map_square_hole_shape_option)
.collect(),
hole_options: row
.hole_options
.into_iter()
.map(map_square_hole_hole_option)
.collect(),
shape_count: row.shape_count,
difficulty: row.difficulty,
publication_status: normalize_square_hole_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,
}
}
fn map_square_hole_run_snapshot(snapshot: SquareHoleRunSnapshot) -> SquareHoleRunRecord {
SquareHoleRunRecord {
run_id: snapshot.run_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
status: normalize_square_hole_run_status(&snapshot.status).to_string(),
snapshot_version: 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),
total_shape_count: snapshot.total_shape_count,
completed_shape_count: snapshot.completed_shape_count,
combo: snapshot.combo,
best_combo: snapshot.best_combo,
score: snapshot.score,
rule_label: snapshot.rule_label,
background_image_src: empty_string_to_none(snapshot.background_image_src),
current_shape: snapshot.current_shape.map(map_square_hole_shape_snapshot),
holes: snapshot
.holes
.into_iter()
.map(map_square_hole_hole_snapshot)
.collect(),
last_feedback: snapshot
.last_feedback
.map(map_square_hole_feedback_snapshot),
last_confirmed_action_id: None,
}
}
fn map_square_hole_shape_snapshot(
snapshot: SquareHoleShapeSnapshot,
) -> SquareHoleShapeSnapshotRecord {
SquareHoleShapeSnapshotRecord {
shape_id: snapshot.shape_id,
shape_kind: snapshot.shape_kind,
label: snapshot.label,
target_hole_id: snapshot.target_hole_id,
color: snapshot.color,
image_src: empty_string_to_none(snapshot.image_src),
}
}
fn map_square_hole_hole_snapshot(snapshot: SquareHoleHoleSnapshot) -> SquareHoleHoleSnapshotRecord {
SquareHoleHoleSnapshotRecord {
hole_id: snapshot.hole_id,
hole_kind: snapshot.hole_kind,
label: snapshot.label,
x: snapshot.x,
y: snapshot.y,
image_src: empty_string_to_none(snapshot.image_src),
}
}
fn map_square_hole_shape_option(
snapshot: SquareHoleShapeOptionSnapshot,
) -> SquareHoleShapeOptionRecord {
SquareHoleShapeOptionRecord {
option_id: snapshot.option_id,
shape_kind: snapshot.shape_kind,
label: snapshot.label,
target_hole_id: snapshot.target_hole_id,
image_prompt: snapshot.image_prompt,
image_src: empty_string_to_none(snapshot.image_src),
}
}
fn map_square_hole_hole_option(
snapshot: SquareHoleHoleOptionSnapshot,
) -> SquareHoleHoleOptionRecord {
SquareHoleHoleOptionRecord {
hole_id: snapshot.hole_id,
hole_kind: snapshot.hole_kind,
label: snapshot.label,
image_prompt: snapshot.image_prompt,
image_src: empty_string_to_none(snapshot.image_src),
}
}
fn map_square_hole_feedback_snapshot(
snapshot: SquareHoleDropFeedbackSnapshot,
) -> SquareHoleDropFeedbackRecord {
SquareHoleDropFeedbackRecord {
accepted: snapshot.accepted,
reject_reason: snapshot
.reject_reason
.map(|value| normalize_square_hole_reject_reason(&value).to_string()),
message: snapshot.message,
}
}
fn build_square_hole_anchor_pack(
config: &SquareHoleCreatorConfigRecord,
) -> SquareHoleAnchorPackRecord {
let shape_count = config.shape_count.to_string();
let difficulty = config.difficulty.to_string();
SquareHoleAnchorPackRecord {
theme: build_square_hole_anchor_item("theme", "题材主题", config.theme_text.as_str()),
twist_rule: build_square_hole_anchor_item(
"twistRule",
"反差规则",
config.twist_rule.as_str(),
),
shape_count: build_square_hole_anchor_item("shapeCount", "形状数量", shape_count.as_str()),
difficulty: build_square_hole_anchor_item("difficulty", "难度", difficulty.as_str()),
}
}
fn build_square_hole_anchor_item(
key: &str,
label: &str,
value: &str,
) -> SquareHoleAnchorItemRecord {
SquareHoleAnchorItemRecord {
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_square_hole_stage(value: &str) -> &str {
match value {
"Collecting" | "CollectingConfig" | "collecting" | "collecting_config" => {
"collecting_config"
}
"ReadyToCompile" | "ready_to_compile" => "ready_to_compile",
"DraftCompiled" | "DraftReady" | "draft_compiled" | "draft_ready" => "draft_ready",
"Published" | "published" => "published",
_ => value,
}
}
fn normalize_square_hole_publication_status(value: &str) -> &str {
match value {
"Draft" | "draft" => "draft",
"Published" | "published" => "published",
_ => value,
}
}
fn normalize_square_hole_run_status(value: &str) -> &str {
match value {
"Running" | "running" => "running",
"Won" | "won" => "won",
"Failed" | "failed" => "failed",
"Stopped" | "stopped" => "stopped",
_ => value,
}
}
fn normalize_square_hole_message_kind(value: &str) -> &str {
match value {
"text" => "chat",
_ => value,
}
}
fn normalize_square_hole_reject_reason(value: &str) -> &str {
match value {
"RunNotActive" | "run_not_active" => "run_not_active",
"SnapshotVersionMismatch" | "snapshot_version_mismatch" => "snapshot_version_mismatch",
"HoleNotFound" | "hole_not_found" => "hole_not_found",
"Incompatible" | "incompatible" => "incompatible",
"TimeUp" | "time_up" => "time_up",
_ => value,
}
}

View File

@@ -0,0 +1,291 @@
use super::*;
impl From<module_runtime::RuntimeSnapshotUpsertInput> for RuntimeSnapshotUpsertInput {
fn from(input: module_runtime::RuntimeSnapshotUpsertInput) -> Self {
Self {
user_id: input.user_id,
saved_at_micros: input.saved_at_micros,
bottom_tab: input.bottom_tab,
game_state_json: input.game_state_json,
current_story_json: input.current_story_json,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<DomainStorySessionInput> for StorySessionInput {
fn from(input: DomainStorySessionInput) -> Self {
Self {
story_session_id: input.story_session_id,
runtime_session_id: input.runtime_session_id,
actor_user_id: input.actor_user_id,
world_profile_id: input.world_profile_id,
initial_prompt: input.initial_prompt,
opening_summary: input.opening_summary,
created_at_micros: input.created_at_micros,
}
}
}
impl From<DomainStoryContinueInput> for StoryContinueInput {
fn from(input: DomainStoryContinueInput) -> Self {
Self {
story_session_id: input.story_session_id,
event_id: input.event_id,
narrative_text: input.narrative_text,
choice_function_id: input.choice_function_id,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<DomainStorySessionStateInput> for StorySessionStateInput {
fn from(input: DomainStorySessionStateInput) -> Self {
Self {
story_session_id: input.story_session_id,
}
}
}
pub(crate) fn map_asset_history_list_result(
result: AssetHistoryListResult,
) -> Result<Vec<AssetHistoryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(map_asset_history_entry_snapshot)
.map(build_asset_history_entry_record)
.collect())
}
pub(crate) fn map_runtime_browse_history_procedure_result(
result: RuntimeBrowseHistoryProcedureResult,
) -> Result<Vec<RuntimeBrowseHistoryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.entries
.into_iter()
.map(|snapshot| {
build_runtime_browse_history_record(map_runtime_browse_history_snapshot(snapshot))
})
.collect())
}
pub(crate) fn map_story_session_procedure_result(
result: StorySessionProcedureResult,
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session 快照"))?;
let event = result
.event
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story event 快照"))?;
Ok(StorySessionResultRecord {
session: map_story_session_snapshot(session),
event: map_story_event_snapshot(event),
})
}
pub(crate) fn map_story_session_state_procedure_result(
result: StorySessionStateProcedureResult,
) -> Result<StorySessionStateRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("story session state 快照"))?;
Ok(StorySessionStateRecord {
session: map_story_session_snapshot(session),
events: result
.events
.into_iter()
.map(map_story_event_snapshot)
.collect(),
})
}
pub(crate) fn map_asset_history_entry_snapshot(
snapshot: AssetHistoryEntrySnapshot,
) -> module_assets::AssetHistoryEntrySnapshot {
module_assets::AssetHistoryEntrySnapshot {
asset_object_id: snapshot.asset_object_id,
asset_kind: snapshot.asset_kind,
image_src: snapshot.image_src,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
entity_id: snapshot.entity_id,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_browse_history_snapshot(
snapshot: RuntimeBrowseHistorySnapshot,
) -> module_runtime::RuntimeBrowseHistorySnapshot {
module_runtime::RuntimeBrowseHistorySnapshot {
browse_history_id: snapshot.browse_history_id,
user_id: snapshot.user_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
world_name: snapshot.world_name,
subtitle: snapshot.subtitle,
summary_text: snapshot.summary_text,
cover_image_src: snapshot.cover_image_src,
theme_mode: map_runtime_browse_history_theme_mode_back(snapshot.theme_mode),
author_display_name: snapshot.author_display_name,
visited_at_micros: snapshot.visited_at_micros,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_snapshot_snapshot(
snapshot: RuntimeSnapshot,
) -> module_runtime::RuntimeSnapshot {
module_runtime::RuntimeSnapshot {
user_id: snapshot.user_id,
version: snapshot.version,
saved_at_micros: snapshot.saved_at_micros,
bottom_tab: snapshot.bottom_tab,
game_state_json: snapshot.game_state_json,
current_story_json: snapshot.current_story_json,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_profile_save_archive_snapshot(
snapshot: RuntimeProfileSaveArchiveSnapshot,
) -> module_runtime::RuntimeProfileSaveArchiveSnapshot {
module_runtime::RuntimeProfileSaveArchiveSnapshot {
archive_id: snapshot.archive_id,
user_id: snapshot.user_id,
world_key: snapshot.world_key,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
world_type: snapshot.world_type,
world_name: snapshot.world_name,
subtitle: snapshot.subtitle,
summary_text: snapshot.summary_text,
cover_image_src: snapshot.cover_image_src,
saved_at_micros: snapshot.saved_at_micros,
bottom_tab: snapshot.bottom_tab,
game_state_json: snapshot.game_state_json,
current_story_json: snapshot.current_story_json,
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_story_session_snapshot(snapshot: StorySessionSnapshot) -> StorySessionRecord {
StorySessionRecord {
story_session_id: snapshot.story_session_id,
runtime_session_id: snapshot.runtime_session_id,
actor_user_id: snapshot.actor_user_id,
world_profile_id: snapshot.world_profile_id,
initial_prompt: snapshot.initial_prompt,
opening_summary: snapshot.opening_summary,
latest_narrative_text: snapshot.latest_narrative_text,
latest_choice_function_id: snapshot.latest_choice_function_id,
status: map_story_session_status(snapshot.status)
.as_str()
.to_string(),
version: snapshot.version,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
pub(crate) fn map_story_event_snapshot(snapshot: StoryEventSnapshot) -> StoryEventRecord {
StoryEventRecord {
event_id: snapshot.event_id,
story_session_id: snapshot.story_session_id,
event_kind: map_story_event_kind(snapshot.event_kind)
.as_str()
.to_string(),
narrative_text: snapshot.narrative_text,
choice_function_id: snapshot.choice_function_id,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
pub(crate) fn map_runtime_browse_history_theme_mode_back(
value: crate::module_bindings::RuntimeBrowseHistoryThemeMode,
) -> module_runtime::RuntimeBrowseHistoryThemeMode {
match value {
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Martial => {
module_runtime::RuntimeBrowseHistoryThemeMode::Martial
}
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Arcane => {
module_runtime::RuntimeBrowseHistoryThemeMode::Arcane
}
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Machina => {
module_runtime::RuntimeBrowseHistoryThemeMode::Machina
}
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Tide => {
module_runtime::RuntimeBrowseHistoryThemeMode::Tide
}
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Rift => {
module_runtime::RuntimeBrowseHistoryThemeMode::Rift
}
crate::module_bindings::RuntimeBrowseHistoryThemeMode::Mythic => {
module_runtime::RuntimeBrowseHistoryThemeMode::Mythic
}
}
}
pub(crate) fn map_story_session_status(value: StorySessionStatus) -> DomainStorySessionStatus {
match value {
StorySessionStatus::Active => DomainStorySessionStatus::Active,
StorySessionStatus::Completed => DomainStorySessionStatus::Completed,
StorySessionStatus::Archived => DomainStorySessionStatus::Archived,
}
}
pub(crate) fn map_story_event_kind(value: StoryEventKind) -> DomainStoryEventKind {
match value {
StoryEventKind::SessionStarted => DomainStoryEventKind::SessionStarted,
StoryEventKind::StoryContinued => DomainStoryEventKind::StoryContinued,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VisualNovelRuntimeEventRecordInput {
pub event_id: String,
pub run_id: String,
pub owner_user_id: String,
pub profile_id: Option<String>,
pub event_kind: String,
pub client_event_id: Option<String>,
pub history_entry_id: Option<String>,
pub payload_json: String,
pub occurred_at_micros: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct VisualNovelRuntimeEventRecord {
pub event_id: String,
pub run_id: Option<String>,
pub owner_user_id: String,
pub profile_id: Option<String>,
pub event_kind: String,
pub client_event_id: Option<String>,
pub history_entry_id: Option<String>,
pub payload: serde_json::Value,
pub occurred_at: String,
}

View File

@@ -0,0 +1,252 @@
use super::*;
pub(crate) fn map_visual_novel_agent_session_procedure_result(
result: VisualNovelAgentSessionProcedureResult,
) -> Result<VisualNovelAgentSessionRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let session = result
.session
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel agent session 快照"))?;
Ok(map_visual_novel_agent_session_snapshot(session))
}
pub(crate) fn map_visual_novel_work_procedure_result(
result: VisualNovelWorkProcedureResult,
) -> Result<VisualNovelWorkProfileRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let work = result
.work
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel work 快照"))?;
Ok(map_visual_novel_work_snapshot(work))
}
pub(crate) fn map_visual_novel_works_procedure_result(
result: VisualNovelWorksProcedureResult,
) -> Result<Vec<VisualNovelWorkProfileRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.items
.into_iter()
.map(map_visual_novel_work_snapshot)
.collect())
}
pub(crate) fn map_visual_novel_run_procedure_result(
result: VisualNovelRunProcedureResult,
) -> Result<VisualNovelRunRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let run = result
.run
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel run 快照"))?;
Ok(map_visual_novel_run_snapshot(run))
}
pub(crate) fn map_visual_novel_history_procedure_result(
result: VisualNovelHistoryProcedureResult,
) -> Result<Vec<VisualNovelHistoryEntryRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result
.items
.into_iter()
.map(map_visual_novel_history_entry)
.collect())
}
pub(crate) fn map_visual_novel_runtime_event_procedure_result(
result: VisualNovelRuntimeEventProcedureResult,
) -> Result<VisualNovelRuntimeEventRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let event = result
.event
.ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel runtime event 快照"))?;
Ok(map_visual_novel_runtime_event(event))
}
fn map_visual_novel_agent_session_snapshot(
snapshot: VisualNovelAgentSessionSnapshot,
) -> VisualNovelAgentSessionRecord {
VisualNovelAgentSessionRecord {
session_id: snapshot.session_id,
owner_user_id: snapshot.owner_user_id,
source_mode: snapshot.source_mode,
status: snapshot.status,
seed_text: snapshot.seed_text,
source_asset_ids: snapshot.source_asset_ids,
current_turn: snapshot.current_turn,
progress_percent: snapshot.progress_percent,
messages: snapshot
.messages
.into_iter()
.map(map_visual_novel_agent_message)
.collect(),
draft: snapshot.draft.map(visual_novel_json_to_value),
pending_action: snapshot.pending_action.map(visual_novel_json_to_value),
last_assistant_reply: snapshot.last_assistant_reply,
published_profile_id: snapshot.published_profile_id,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_visual_novel_agent_message(
snapshot: VisualNovelAgentMessageSnapshot,
) -> VisualNovelAgentMessageRecord {
VisualNovelAgentMessageRecord {
message_id: snapshot.message_id,
session_id: snapshot.session_id,
role: snapshot.role,
kind: snapshot.kind,
text: snapshot.text,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_visual_novel_work_snapshot(
snapshot: VisualNovelWorkSnapshot,
) -> VisualNovelWorkProfileRecord {
VisualNovelWorkProfileRecord {
work_id: snapshot.work_id,
profile_id: snapshot.profile_id,
owner_user_id: snapshot.owner_user_id,
source_session_id: snapshot.source_session_id,
author_display_name: snapshot.author_display_name,
work_title: snapshot.work_title,
work_description: snapshot.work_description,
tags: snapshot.tags,
cover_image_src: snapshot.cover_image_src,
source_asset_ids: snapshot.source_asset_ids,
draft: visual_novel_json_to_value(snapshot.draft),
publication_status: snapshot.publication_status,
publish_ready: snapshot.publish_ready,
play_count: snapshot.play_count,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
}
}
pub(crate) fn map_visual_novel_gallery_view_row(
row: VisualNovelGalleryViewRow,
) -> VisualNovelWorkProfileRecord {
VisualNovelWorkProfileRecord {
work_id: row.work_id,
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
source_session_id: row.source_session_id,
author_display_name: row.author_display_name,
work_title: row.work_title,
work_description: row.work_description,
tags: row.tags,
cover_image_src: row.cover_image_src,
source_asset_ids: row.source_asset_ids,
// 中文注释:公开列表 view 不暴露完整 draft详情页仍通过 detail procedure 读取。
draft: serde_json::Value::Null,
publication_status: row.publication_status,
publish_ready: row.publish_ready,
play_count: row.play_count,
created_at: format_timestamp_micros(row.created_at_micros),
updated_at: format_timestamp_micros(row.updated_at_micros),
published_at: row.published_at_micros.map(format_timestamp_micros),
}
}
fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunSnapshot) -> VisualNovelRunRecord {
VisualNovelRunRecord {
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
mode: snapshot.mode,
status: snapshot.status,
current_scene_id: snapshot.current_scene_id,
current_phase_id: snapshot.current_phase_id,
visible_character_ids: snapshot.visible_character_ids,
flags: visual_novel_json_to_value(snapshot.flags),
metrics: visual_novel_json_to_value(snapshot.metrics),
history: snapshot
.history
.into_iter()
.map(map_visual_novel_history_entry)
.collect(),
available_choices: visual_novel_json_to_value(snapshot.available_choices),
text_mode_enabled: snapshot.text_mode_enabled,
created_at: format_timestamp_micros(snapshot.created_at_micros),
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
}
}
fn map_visual_novel_history_entry(
snapshot: VisualNovelRuntimeHistoryEntrySnapshot,
) -> VisualNovelHistoryEntryRecord {
VisualNovelHistoryEntryRecord {
entry_id: snapshot.entry_id,
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
turn_index: snapshot.turn_index,
source: snapshot.source,
action_text: snapshot.action_text,
steps: visual_novel_json_to_value(snapshot.steps),
snapshot_before_hash: snapshot.snapshot_before_hash,
snapshot_after_hash: snapshot.snapshot_after_hash,
created_at: format_timestamp_micros(snapshot.created_at_micros),
}
}
fn map_visual_novel_runtime_event(
snapshot: VisualNovelRuntimeEventSnapshot,
) -> VisualNovelRuntimeEventRecord {
VisualNovelRuntimeEventRecord {
event_id: snapshot.event_id,
run_id: snapshot.run_id,
owner_user_id: snapshot.owner_user_id,
profile_id: snapshot.profile_id,
event_kind: snapshot.event_kind,
client_event_id: snapshot.client_event_id,
history_entry_id: snapshot.history_entry_id,
payload: visual_novel_json_to_value(snapshot.payload),
occurred_at: format_timestamp_micros(snapshot.occurred_at_micros),
}
}
fn visual_novel_json_to_value(value: VisualNovelJsonValue) -> serde_json::Value {
match value {
VisualNovelJsonValue::Null => serde_json::Value::Null,
VisualNovelJsonValue::Bool(value) => serde_json::Value::Bool(value),
VisualNovelJsonValue::Number(value) => serde_json::Number::from_f64(value)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
VisualNovelJsonValue::String(value) => serde_json::Value::String(value),
VisualNovelJsonValue::Array(items) => {
serde_json::Value::Array(items.into_iter().map(visual_novel_json_to_value).collect())
}
VisualNovelJsonValue::Object(fields) => {
let object = fields
.into_iter()
.map(|field| (field.key, visual_novel_json_to_value(field.value)))
.collect();
serde_json::Value::Object(object)
}
}
}