Files
Genarrative/server-rs/crates/spacetime-client/src/mapper/big_fish.rs

621 lines
24 KiB
Rust

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,
level_background_image_src: None,
level_background_image_object_key: None,
ui_spritesheet_image_src: None,
ui_spritesheet_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,
}
}
}