271 lines
9.8 KiB
Rust
271 lines
9.8 KiB
Rust
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<BigFishSessionSnapshot, 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()))?;
|
||
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<BigFishSessionSnapshot, String> {
|
||
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<BigFishAssetSlotSnapshot> {
|
||
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::<Vec<_>>();
|
||
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),
|
||
});
|
||
}
|