use crate::big_fish::tables::{big_fish_asset_slot, big_fish_creation_session}; use crate::*; use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness}; #[spacetimedb::procedure] pub fn generate_big_fish_asset( ctx: &mut ProcedureContext, input: BigFishAssetGenerateInput, ) -> BigFishSessionProcedureResult { match ctx.try_with_tx(|tx| generate_big_fish_asset_tx(tx, input.clone())) { Ok(session) => BigFishSessionProcedureResult { ok: true, session: Some(session), error_message: None, }, Err(message) => BigFishSessionProcedureResult { ok: false, session: None, error_message: Some(message), }, } } #[spacetimedb::procedure] pub fn publish_big_fish_game( ctx: &mut ProcedureContext, input: BigFishPublishInput, ) -> BigFishSessionProcedureResult { match ctx.try_with_tx(|tx| publish_big_fish_game_tx(tx, input.clone())) { Ok(session) => BigFishSessionProcedureResult { ok: true, session: Some(session), error_message: None, }, Err(message) => BigFishSessionProcedureResult { ok: false, session: None, error_message: Some(message), }, } } pub(crate) fn generate_big_fish_asset_tx( ctx: &ReducerContext, input: BigFishAssetGenerateInput, ) -> Result { let session = ctx .db .big_fish_creation_session() .session_id() .find(&input.session_id) .filter(|row| row.owner_user_id == input.owner_user_id) .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; let draft = session .draft_json .as_deref() .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; validate_asset_generate_input(&input, &draft).map_err(|error| error.to_string())?; let slot = build_generated_asset_slot( &input.session_id, &draft, input.asset_kind, input.level, input.motion_key.clone(), input.asset_url.clone(), input.generated_at_micros, ) .map_err(|error| error.to_string())?; upsert_big_fish_asset_slot(ctx, slot); let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id); let readiness = evaluate_publish_readiness( EvaluateBigFishPublishReadinessCommand { session_id: session.session_id.clone(), owner_user_id: session.owner_user_id.clone(), draft: Some(draft.clone()), evaluated_at_micros: input.generated_at_micros, }, &asset_slots, ) .map_err(|error| error.to_string())?; let coverage = build_asset_coverage(Some(&draft), &asset_slots); let updated_at = Timestamp::from_micros_since_unix_epoch(input.generated_at_micros); let uses_placeholder = input .asset_url .as_deref() .map(str::trim) .is_none_or(str::is_empty); let reply = match (input.asset_kind, uses_placeholder) { (BigFishAssetKind::LevelMainImage, true) => "本级主图占位图已生成,可在结果页继续预览。", (BigFishAssetKind::LevelMainImage, false) => "本级主图已正式生成,可在结果页继续预览。", (BigFishAssetKind::LevelMotion, true) => "本级动作占位图已生成,可在结果页继续预览。", (BigFishAssetKind::LevelMotion, false) => "本级动作图已正式生成,可在结果页继续预览。", (BigFishAssetKind::StageBackground, true) => { "活动区域背景占位图已生成,可在结果页继续预览。" } (BigFishAssetKind::StageBackground, false) => { "活动区域背景已正式生成,可在结果页继续预览。" } } .to_string(); let next_stage = if readiness.readiness.publish_ready { BigFishCreationStage::ReadyToPublish } else { BigFishCreationStage::AssetRefining }; let next_session = BigFishCreationSession { session_id: session.session_id.clone(), owner_user_id: session.owner_user_id.clone(), seed_text: session.seed_text.clone(), current_turn: session.current_turn, progress_percent: if readiness.readiness.publish_ready { 96 } else { 88 }, stage: next_stage, anchor_pack_json: session.anchor_pack_json.clone(), draft_json: session.draft_json.clone(), asset_coverage_json: serialize_asset_coverage(&coverage) .map_err(|error| error.to_string())?, last_assistant_reply: Some(reply.clone()), publish_ready: readiness.readiness.publish_ready, play_count: session.play_count, remix_count: session.remix_count, like_count: session.like_count, published_at: session.published_at, created_at: session.created_at, updated_at, visible: session.visible, }; replace_big_fish_session(ctx, &session, next_session); for event in readiness.events { emit_big_fish_publish_readiness_event(ctx, event)?; } get_big_fish_session_tx( ctx, BigFishSessionGetInput { session_id: input.session_id, owner_user_id: input.owner_user_id, }, ) } pub(crate) fn publish_big_fish_game_tx( ctx: &ReducerContext, input: BigFishPublishInput, ) -> Result { validate_publish_input(&input).map_err(|error| error.to_string())?; let session = ctx .db .big_fish_creation_session() .session_id() .find(&input.session_id) .filter(|row| row.owner_user_id == input.owner_user_id) .ok_or_else(|| "big_fish_creation_session 不存在".to_string())?; let draft = session .draft_json .as_deref() .ok_or_else(|| "big_fish.draft 尚未编译".to_string()) .and_then(|value| deserialize_draft(value).map_err(|error| error.to_string()))?; let asset_slots = list_big_fish_asset_slots(ctx, &session.session_id); let readiness = evaluate_publish_readiness( EvaluateBigFishPublishReadinessCommand { session_id: session.session_id.clone(), owner_user_id: session.owner_user_id.clone(), draft: Some(draft.clone()), evaluated_at_micros: input.published_at_micros, }, &asset_slots, ) .map_err(|error| error.to_string())?; let coverage = build_asset_coverage(Some(&draft), &asset_slots); if !readiness.readiness.publish_ready { return Err(format!( "big_fish 发布校验未通过:{}", readiness.readiness.blockers.join(";") )); } let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); let next_session = BigFishCreationSession { session_id: session.session_id.clone(), owner_user_id: session.owner_user_id.clone(), seed_text: session.seed_text.clone(), current_turn: session.current_turn, progress_percent: 100, stage: BigFishCreationStage::Published, anchor_pack_json: session.anchor_pack_json.clone(), draft_json: session.draft_json.clone(), asset_coverage_json: serialize_asset_coverage(&coverage) .map_err(|error| error.to_string())?, last_assistant_reply: Some("玩法已发布,可以进入测试运行态。".to_string()), publish_ready: true, play_count: session.play_count, remix_count: session.remix_count, like_count: session.like_count, published_at: Some(published_at), created_at: session.created_at, updated_at: published_at, visible: session.visible, }; replace_big_fish_session(ctx, &session, next_session); for event in readiness.events { emit_big_fish_publish_readiness_event(ctx, event)?; } get_big_fish_session_tx( ctx, BigFishSessionGetInput { session_id: input.session_id, owner_user_id: input.owner_user_id, }, ) } pub(crate) fn list_big_fish_asset_slots( ctx: &ReducerContext, session_id: &str, ) -> Vec { let mut slots = ctx .db .big_fish_asset_slot() .by_big_fish_asset_session_id() .filter(&session_id.to_string()) .map(|slot| BigFishAssetSlotSnapshot { slot_id: slot.slot_id, session_id: slot.session_id, asset_kind: slot.asset_kind, level: slot.level, motion_key: slot.motion_key, status: slot.status, asset_url: slot.asset_url, prompt_snapshot: slot.prompt_snapshot, updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(), }) .collect::>(); slots.sort_by_key(|slot| { ( slot.level.unwrap_or(0), slot.asset_kind.as_str().to_string(), slot.motion_key.clone().unwrap_or_default(), slot.slot_id.clone(), ) }); slots } pub(crate) fn upsert_big_fish_asset_slot(ctx: &ReducerContext, slot: BigFishAssetSlotSnapshot) { if let Some(existing) = ctx.db.big_fish_asset_slot().slot_id().find(&slot.slot_id) { ctx.db .big_fish_asset_slot() .slot_id() .delete(&existing.slot_id); } ctx.db.big_fish_asset_slot().insert(BigFishAssetSlot { slot_id: slot.slot_id, session_id: slot.session_id, asset_kind: slot.asset_kind, level: slot.level, motion_key: slot.motion_key, status: slot.status, asset_url: slot.asset_url, prompt_snapshot: slot.prompt_snapshot, updated_at: Timestamp::from_micros_since_unix_epoch(slot.updated_at_micros), }); }