1
This commit is contained in:
@@ -109,6 +109,9 @@ pub(crate) fn generate_big_fish_asset_tx(
|
||||
last_assistant_reply: Some(reply.clone()),
|
||||
publish_ready: coverage.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,
|
||||
};
|
||||
@@ -166,6 +169,9 @@ pub(crate) fn publish_big_fish_game_tx(
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -122,6 +122,25 @@ pub fn record_big_fish_play(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn remix_big_fish_work(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BigFishWorkRemixInput,
|
||||
) -> BigFishSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| remix_big_fish_work_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 submit_big_fish_message(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -224,6 +243,9 @@ pub(crate) fn create_big_fish_session_tx(
|
||||
last_assistant_reply: Some(input.welcome_message_text.clone()),
|
||||
publish_ready: false,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
published_at: None,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
});
|
||||
@@ -414,6 +436,9 @@ pub(crate) fn submit_big_fish_message_tx(
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.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: submitted_at,
|
||||
};
|
||||
@@ -461,6 +486,9 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||
publish_ready: session.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,
|
||||
};
|
||||
@@ -516,6 +544,9 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
||||
last_assistant_reply: Some(assistant_reply_text),
|
||||
publish_ready: session.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,
|
||||
};
|
||||
@@ -570,6 +601,9 @@ pub(crate) fn compile_big_fish_draft_tx(
|
||||
last_assistant_reply: Some(reply.clone()),
|
||||
publish_ready: coverage.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: compiled_at,
|
||||
};
|
||||
@@ -656,6 +690,9 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
publish_ready: session.publish_ready,
|
||||
// 中文注释:正式进入已发布作品时同时累加作品播放数,用户侧去重由 profile_played_world 保证。
|
||||
play_count: session.play_count.saturating_add(1),
|
||||
remix_count: session.remix_count,
|
||||
like_count: session.like_count,
|
||||
published_at: session.published_at,
|
||||
created_at: session.created_at,
|
||||
updated_at: played_at,
|
||||
};
|
||||
@@ -670,6 +707,123 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
)
|
||||
}
|
||||
|
||||
fn remix_big_fish_work_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BigFishWorkRemixInput,
|
||||
) -> Result<BigFishSessionSnapshot, String> {
|
||||
let source_session_id = input.source_session_id.trim();
|
||||
let target_session_id = input.target_session_id.trim();
|
||||
let target_owner_user_id = input.target_owner_user_id.trim();
|
||||
let welcome_message_id = input.welcome_message_id.trim();
|
||||
if source_session_id.is_empty()
|
||||
|| target_session_id.is_empty()
|
||||
|| target_owner_user_id.is_empty()
|
||||
|| welcome_message_id.is_empty()
|
||||
{
|
||||
return Err("big_fish remix 参数不能为空".to_string());
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&target_session_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish remix 目标 session 已存在".to_string());
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.big_fish_agent_message()
|
||||
.message_id()
|
||||
.find(&welcome_message_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("big_fish remix 消息已存在".to_string());
|
||||
}
|
||||
|
||||
let source = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&source_session_id.to_string())
|
||||
.filter(|row| row.stage == BigFishCreationStage::Published)
|
||||
.ok_or_else(|| "big_fish 已发布源作品不存在".to_string())?;
|
||||
let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros);
|
||||
let next_source = BigFishCreationSession {
|
||||
session_id: source.session_id.clone(),
|
||||
owner_user_id: source.owner_user_id.clone(),
|
||||
seed_text: source.seed_text.clone(),
|
||||
current_turn: source.current_turn,
|
||||
progress_percent: source.progress_percent,
|
||||
stage: source.stage,
|
||||
anchor_pack_json: source.anchor_pack_json.clone(),
|
||||
draft_json: source.draft_json.clone(),
|
||||
asset_coverage_json: source.asset_coverage_json.clone(),
|
||||
last_assistant_reply: source.last_assistant_reply.clone(),
|
||||
publish_ready: source.publish_ready,
|
||||
play_count: source.play_count,
|
||||
remix_count: source.remix_count.saturating_add(1),
|
||||
like_count: source.like_count,
|
||||
published_at: source.published_at,
|
||||
created_at: source.created_at,
|
||||
updated_at: remixed_at,
|
||||
};
|
||||
replace_big_fish_session(ctx, &source, next_source);
|
||||
|
||||
let target_session = BigFishCreationSession {
|
||||
session_id: target_session_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
seed_text: source.seed_text.clone(),
|
||||
current_turn: 1,
|
||||
progress_percent: 80,
|
||||
stage: BigFishCreationStage::DraftReady,
|
||||
anchor_pack_json: source.anchor_pack_json.clone(),
|
||||
draft_json: source.draft_json.clone(),
|
||||
asset_coverage_json: source.asset_coverage_json.clone(),
|
||||
last_assistant_reply: Some("已从公开作品 Remix 出新的大鱼吃小鱼草稿。".to_string()),
|
||||
publish_ready: source.publish_ready,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
published_at: None,
|
||||
created_at: remixed_at,
|
||||
updated_at: remixed_at,
|
||||
};
|
||||
ctx.db.big_fish_creation_session().insert(target_session);
|
||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||
message_id: welcome_message_id.to_string(),
|
||||
session_id: target_session_id.to_string(),
|
||||
role: BigFishAgentMessageRole::Assistant,
|
||||
kind: BigFishAgentMessageKind::Summary,
|
||||
text: "已复制公开作品为你的草稿。".to_string(),
|
||||
created_at: remixed_at,
|
||||
});
|
||||
for slot in list_big_fish_asset_slots(ctx, &source.session_id) {
|
||||
upsert_big_fish_asset_slot(
|
||||
ctx,
|
||||
BigFishAssetSlotSnapshot {
|
||||
slot_id: slot.slot_id.replace(&source.session_id, target_session_id),
|
||||
session_id: target_session_id.to_string(),
|
||||
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: input.remixed_at_micros,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get_big_fish_session_tx(
|
||||
ctx,
|
||||
BigFishSessionGetInput {
|
||||
session_id: target_session_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_big_fish_session_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
@@ -784,6 +938,12 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
level_motion_ready_count: coverage.level_motion_ready_count,
|
||||
background_ready: coverage.background_ready,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -821,6 +981,13 @@ mod tests {
|
||||
last_assistant_reply: Some("欢迎来到大鱼吃小鱼共创。".to_string()),
|
||||
publish_ready: false,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
published_at: if stage == BigFishCreationStage::Published {
|
||||
Some(Timestamp::from_micros_since_unix_epoch(1))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ pub struct BigFishCreationSession {
|
||||
pub(crate) last_assistant_reply: Option<String>,
|
||||
pub(crate) publish_ready: bool,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) remix_count: u32,
|
||||
pub(crate) like_count: u32,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
@@ -337,6 +337,10 @@ pub struct CustomWorldProfile {
|
||||
profile_payload_json: String,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
author_display_name: String,
|
||||
published_at: Option<Timestamp>,
|
||||
// 软删除后保留 profile 真相,供审计与幂等删除使用。
|
||||
@@ -484,6 +488,10 @@ pub struct CustomWorldGalleryEntry {
|
||||
theme_mode: CustomWorldThemeMode,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
published_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
}
|
||||
@@ -2161,6 +2169,48 @@ pub fn get_custom_world_gallery_detail_by_code(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn remix_custom_world_profile(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: module_custom_world::CustomWorldProfileRemixInput,
|
||||
) -> CustomWorldLibraryMutationResult {
|
||||
match ctx.try_with_tx(|tx| remix_custom_world_profile_record(tx, input.clone())) {
|
||||
Ok((entry, gallery_entry)) => CustomWorldLibraryMutationResult {
|
||||
ok: true,
|
||||
entry: Some(entry),
|
||||
gallery_entry,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => CustomWorldLibraryMutationResult {
|
||||
ok: false,
|
||||
entry: None,
|
||||
gallery_entry: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn record_custom_world_profile_play(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: module_custom_world::CustomWorldProfilePlayRecordInput,
|
||||
) -> CustomWorldLibraryMutationResult {
|
||||
match ctx.try_with_tx(|tx| record_custom_world_profile_play_record(tx, input.clone())) {
|
||||
Ok((entry, gallery_entry)) => CustomWorldLibraryMutationResult {
|
||||
ok: true,
|
||||
entry: Some(entry),
|
||||
gallery_entry: Some(gallery_entry),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => CustomWorldLibraryMutationResult {
|
||||
ok: false,
|
||||
entry: None,
|
||||
gallery_entry: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn list_custom_world_works(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -2466,6 +2516,9 @@ fn upsert_custom_world_profile_record(
|
||||
profile_payload_json: input.profile_payload_json.clone(),
|
||||
playable_npc_count: input.playable_npc_count,
|
||||
landmark_count: input.landmark_count,
|
||||
play_count: existing.play_count,
|
||||
remix_count: existing.remix_count,
|
||||
like_count: existing.like_count,
|
||||
author_display_name: input.author_display_name.clone(),
|
||||
published_at: existing.published_at,
|
||||
deleted_at: None,
|
||||
@@ -2488,6 +2541,9 @@ fn upsert_custom_world_profile_record(
|
||||
profile_payload_json: input.profile_payload_json.clone(),
|
||||
playable_npc_count: input.playable_npc_count,
|
||||
landmark_count: input.landmark_count,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: input.author_display_name.clone(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
@@ -2632,6 +2688,9 @@ fn publish_custom_world_profile_record(
|
||||
profile_payload_json: existing.profile_payload_json.clone(),
|
||||
playable_npc_count: existing.playable_npc_count,
|
||||
landmark_count: existing.landmark_count,
|
||||
play_count: existing.play_count,
|
||||
remix_count: existing.remix_count,
|
||||
like_count: existing.like_count,
|
||||
author_display_name: input.author_display_name.clone(),
|
||||
published_at: Some(published_at),
|
||||
deleted_at: None,
|
||||
@@ -2695,6 +2754,9 @@ fn unpublish_custom_world_profile_record(
|
||||
profile_payload_json: existing.profile_payload_json.clone(),
|
||||
playable_npc_count: existing.playable_npc_count,
|
||||
landmark_count: existing.landmark_count,
|
||||
play_count: existing.play_count,
|
||||
remix_count: existing.remix_count,
|
||||
like_count: existing.like_count,
|
||||
author_display_name: input.author_display_name.clone(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
@@ -2754,6 +2816,9 @@ fn delete_custom_world_profile_record(
|
||||
profile_payload_json: existing.profile_payload_json.clone(),
|
||||
playable_npc_count: existing.playable_npc_count,
|
||||
landmark_count: existing.landmark_count,
|
||||
play_count: existing.play_count,
|
||||
remix_count: existing.remix_count,
|
||||
like_count: existing.like_count,
|
||||
author_display_name: existing.author_display_name.clone(),
|
||||
published_at: None,
|
||||
deleted_at: Some(deleted_at),
|
||||
@@ -2924,6 +2989,177 @@ fn get_custom_world_gallery_detail_record_by_code(
|
||||
))
|
||||
}
|
||||
|
||||
fn remix_custom_world_profile_record(
|
||||
ctx: &ReducerContext,
|
||||
input: module_custom_world::CustomWorldProfileRemixInput,
|
||||
) -> Result<
|
||||
(
|
||||
CustomWorldProfileSnapshot,
|
||||
Option<CustomWorldGalleryEntrySnapshot>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let source_owner_user_id = input.source_owner_user_id.trim();
|
||||
let source_profile_id = input.source_profile_id.trim();
|
||||
let target_owner_user_id = input.target_owner_user_id.trim();
|
||||
let target_profile_id = input.target_profile_id.trim();
|
||||
if source_owner_user_id.is_empty()
|
||||
|| source_profile_id.is_empty()
|
||||
|| target_owner_user_id.is_empty()
|
||||
|| target_profile_id.is_empty()
|
||||
{
|
||||
return Err("custom_world remix 参数不能为空".to_string());
|
||||
}
|
||||
if input.author_display_name.trim().is_empty() {
|
||||
return Err("custom_world remix 作者名不能为空".to_string());
|
||||
}
|
||||
|
||||
// Remix 只允许从已发布源作品派生草稿,同时把源作品的公开 remix 计数同步到画廊。
|
||||
let source = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&source_profile_id.to_string())
|
||||
.filter(|row| row.owner_user_id == source_owner_user_id)
|
||||
.filter(|row| {
|
||||
row.publication_status == CustomWorldPublicationStatus::Published
|
||||
&& row.deleted_at.is_none()
|
||||
&& row.published_at.is_some()
|
||||
})
|
||||
.ok_or_else(|| "custom_world 已发布源作品不存在".to_string())?;
|
||||
if ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&target_profile_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("custom_world remix 目标 profile 已存在".to_string());
|
||||
}
|
||||
|
||||
let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros);
|
||||
let next_source = CustomWorldProfile {
|
||||
profile_id: source.profile_id.clone(),
|
||||
owner_user_id: source.owner_user_id.clone(),
|
||||
public_work_code: source.public_work_code.clone(),
|
||||
author_public_user_code: source.author_public_user_code.clone(),
|
||||
source_agent_session_id: source.source_agent_session_id.clone(),
|
||||
publication_status: source.publication_status,
|
||||
world_name: source.world_name.clone(),
|
||||
subtitle: source.subtitle.clone(),
|
||||
summary_text: source.summary_text.clone(),
|
||||
theme_mode: source.theme_mode,
|
||||
cover_image_src: source.cover_image_src.clone(),
|
||||
profile_payload_json: source.profile_payload_json.clone(),
|
||||
playable_npc_count: source.playable_npc_count,
|
||||
landmark_count: source.landmark_count,
|
||||
play_count: source.play_count,
|
||||
remix_count: source.remix_count.saturating_add(1),
|
||||
like_count: source.like_count,
|
||||
author_display_name: source.author_display_name.clone(),
|
||||
published_at: source.published_at,
|
||||
deleted_at: source.deleted_at,
|
||||
created_at: source.created_at,
|
||||
updated_at: remixed_at,
|
||||
};
|
||||
ctx.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.delete(&source.profile_id);
|
||||
let updated_source = ctx.db.custom_world_profile().insert(next_source);
|
||||
let source_gallery = sync_custom_world_gallery_entry_from_profile(ctx, &updated_source)?;
|
||||
|
||||
// 新草稿继承作品内容,但互动计数从 0 开始,避免把源作品热度复制成用户资产。
|
||||
let draft = CustomWorldProfile {
|
||||
profile_id: target_profile_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
public_work_code: None,
|
||||
author_public_user_code: None,
|
||||
source_agent_session_id: None,
|
||||
publication_status: CustomWorldPublicationStatus::Draft,
|
||||
world_name: source.world_name.clone(),
|
||||
subtitle: source.subtitle.clone(),
|
||||
summary_text: source.summary_text.clone(),
|
||||
theme_mode: source.theme_mode,
|
||||
cover_image_src: source.cover_image_src.clone(),
|
||||
profile_payload_json: source.profile_payload_json.clone(),
|
||||
playable_npc_count: source.playable_npc_count,
|
||||
landmark_count: source.landmark_count,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: input.author_display_name.trim().to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
created_at: remixed_at,
|
||||
updated_at: remixed_at,
|
||||
};
|
||||
let inserted_draft = ctx.db.custom_world_profile().insert(draft);
|
||||
Ok((
|
||||
build_custom_world_profile_snapshot(&inserted_draft),
|
||||
Some(source_gallery),
|
||||
))
|
||||
}
|
||||
|
||||
fn record_custom_world_profile_play_record(
|
||||
ctx: &ReducerContext,
|
||||
input: module_custom_world::CustomWorldProfilePlayRecordInput,
|
||||
) -> Result<(CustomWorldProfileSnapshot, CustomWorldGalleryEntrySnapshot), String> {
|
||||
let owner_user_id = input.owner_user_id.trim();
|
||||
let profile_id = input.profile_id.trim();
|
||||
if owner_user_id.is_empty() || profile_id.is_empty() {
|
||||
return Err("custom_world play 参数不能为空".to_string());
|
||||
}
|
||||
let existing = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.filter(|row| row.owner_user_id == owner_user_id)
|
||||
.filter(|row| {
|
||||
row.publication_status == CustomWorldPublicationStatus::Published
|
||||
&& row.deleted_at.is_none()
|
||||
&& row.published_at.is_some()
|
||||
})
|
||||
.ok_or_else(|| "custom_world 已发布作品不存在,无法记录游玩".to_string())?;
|
||||
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
// 游玩计数是公开广场消费数据,只增加计数并保持作品内容不变。
|
||||
let next_row = CustomWorldProfile {
|
||||
profile_id: existing.profile_id.clone(),
|
||||
owner_user_id: existing.owner_user_id.clone(),
|
||||
public_work_code: existing.public_work_code.clone(),
|
||||
author_public_user_code: existing.author_public_user_code.clone(),
|
||||
source_agent_session_id: existing.source_agent_session_id.clone(),
|
||||
publication_status: existing.publication_status,
|
||||
world_name: existing.world_name.clone(),
|
||||
subtitle: existing.subtitle.clone(),
|
||||
summary_text: existing.summary_text.clone(),
|
||||
theme_mode: existing.theme_mode,
|
||||
cover_image_src: existing.cover_image_src.clone(),
|
||||
profile_payload_json: existing.profile_payload_json.clone(),
|
||||
playable_npc_count: existing.playable_npc_count,
|
||||
landmark_count: existing.landmark_count,
|
||||
play_count: existing.play_count.saturating_add(1),
|
||||
remix_count: existing.remix_count,
|
||||
like_count: existing.like_count,
|
||||
author_display_name: existing.author_display_name.clone(),
|
||||
published_at: existing.published_at,
|
||||
deleted_at: existing.deleted_at,
|
||||
created_at: existing.created_at,
|
||||
updated_at: played_at,
|
||||
};
|
||||
ctx.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.delete(&existing.profile_id);
|
||||
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
||||
let gallery_entry = sync_custom_world_gallery_entry_from_profile(ctx, &inserted)?;
|
||||
Ok((
|
||||
build_custom_world_profile_snapshot(&inserted),
|
||||
gallery_entry,
|
||||
))
|
||||
}
|
||||
|
||||
fn list_custom_world_work_snapshots(
|
||||
ctx: &ReducerContext,
|
||||
input: CustomWorldWorksListInput,
|
||||
@@ -5054,6 +5290,9 @@ fn sync_custom_world_gallery_entry_from_profile(
|
||||
theme_mode: profile.theme_mode,
|
||||
playable_npc_count: profile.playable_npc_count,
|
||||
landmark_count: profile.landmark_count,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
like_count: profile.like_count,
|
||||
published_at,
|
||||
updated_at: profile.updated_at,
|
||||
};
|
||||
@@ -5135,6 +5374,9 @@ fn ensure_custom_world_profile_public_fields(
|
||||
profile_payload_json: profile.profile_payload_json.clone(),
|
||||
playable_npc_count: profile.playable_npc_count,
|
||||
landmark_count: profile.landmark_count,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
like_count: profile.like_count,
|
||||
author_display_name: profile.author_display_name.clone(),
|
||||
published_at: profile.published_at,
|
||||
deleted_at: profile.deleted_at,
|
||||
@@ -5161,6 +5403,9 @@ fn build_custom_world_profile_row_copy(profile: &CustomWorldProfile) -> CustomWo
|
||||
profile_payload_json: profile.profile_payload_json.clone(),
|
||||
playable_npc_count: profile.playable_npc_count,
|
||||
landmark_count: profile.landmark_count,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
like_count: profile.like_count,
|
||||
author_display_name: profile.author_display_name.clone(),
|
||||
published_at: profile.published_at,
|
||||
deleted_at: profile.deleted_at,
|
||||
@@ -5185,6 +5430,9 @@ fn build_custom_world_profile_snapshot(row: &CustomWorldProfile) -> CustomWorldP
|
||||
profile_payload_json: row.profile_payload_json.clone(),
|
||||
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,
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
@@ -5337,6 +5585,9 @@ fn build_custom_world_gallery_entry_snapshot(
|
||||
theme_mode: row.theme_mode,
|
||||
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,
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
@@ -6405,6 +6656,9 @@ mod tests {
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
@@ -6426,6 +6680,9 @@ mod tests {
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: Some(Timestamp::from_micros_since_unix_epoch(2)),
|
||||
@@ -6447,6 +6704,9 @@ mod tests {
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
@@ -6507,6 +6767,9 @@ mod tests {
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: if publication_status == CustomWorldPublicationStatus::Published {
|
||||
Some(Timestamp::from_micros_since_unix_epoch(2))
|
||||
@@ -6568,6 +6831,9 @@ mod tests {
|
||||
profile_payload_json: "{}".to_string(),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
author_display_name: "玩家".to_string(),
|
||||
published_at: None,
|
||||
deleted_at: None,
|
||||
|
||||
@@ -669,6 +669,43 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
object
|
||||
.entry("play_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("remix_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("like_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("published_at".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "custom_world_profile" || table_name == "custom_world_gallery_entry" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:自定义世界公开互动计数字段晚于基础作品表加入,旧迁移包按 0 兼容。
|
||||
object
|
||||
.entry("play_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("remix_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("like_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
}
|
||||
if table_name == "puzzle_work_profile" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:拼图公开互动计数晚于基础作品表加入,旧迁移包按 0 兼容。
|
||||
object
|
||||
.entry("play_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("remix_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
object
|
||||
.entry("like_count".to_string())
|
||||
.or_insert_with(|| serde_json::Value::from(0));
|
||||
}
|
||||
}
|
||||
next_value
|
||||
|
||||
@@ -11,7 +11,7 @@ use module_puzzle::{
|
||||
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult, PuzzleRunSnapshot,
|
||||
PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput,
|
||||
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
|
||||
PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
apply_publish_overrides_to_draft, apply_selected_candidate, build_result_preview,
|
||||
compile_result_draft, create_work_profile, infer_anchor_pack, normalize_theme_tags,
|
||||
publish_work_profile, resolve_puzzle_grid_size, select_next_profile, start_run, swap_pieces,
|
||||
@@ -77,6 +77,8 @@ pub struct PuzzleWorkProfileRow {
|
||||
cover_asset_id: Option<String>,
|
||||
publication_status: PuzzlePublicationStatus,
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
anchor_pack_json: String,
|
||||
publish_ready: bool,
|
||||
created_at: Timestamp,
|
||||
@@ -387,6 +389,25 @@ pub fn get_puzzle_gallery_detail(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn remix_puzzle_work(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleWorkRemixInput,
|
||||
) -> PuzzleAgentSessionProcedureResult {
|
||||
match ctx.try_with_tx(|tx| remix_puzzle_work_tx(tx, input.clone())) {
|
||||
Ok(session) => PuzzleAgentSessionProcedureResult {
|
||||
ok: true,
|
||||
session_json: Some(serialize_json(&session)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleAgentSessionProcedureResult {
|
||||
ok: false,
|
||||
session_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn start_puzzle_run(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -931,6 +952,8 @@ fn update_puzzle_work_tx(
|
||||
cover_asset_id: input.cover_asset_id,
|
||||
publication_status: row.publication_status,
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
@@ -1033,6 +1056,140 @@ fn get_puzzle_gallery_detail_tx(
|
||||
build_puzzle_work_profile_from_row(&row)
|
||||
}
|
||||
|
||||
fn remix_puzzle_work_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleWorkRemixInput,
|
||||
) -> Result<PuzzleAgentSessionSnapshot, String> {
|
||||
let source_profile_id = input.source_profile_id.trim();
|
||||
let target_owner_user_id = input.target_owner_user_id.trim();
|
||||
let target_session_id = input.target_session_id.trim();
|
||||
let target_profile_id = input.target_profile_id.trim();
|
||||
let target_work_id = input.target_work_id.trim();
|
||||
if source_profile_id.is_empty()
|
||||
|| target_owner_user_id.is_empty()
|
||||
|| target_session_id.is_empty()
|
||||
|| target_profile_id.is_empty()
|
||||
|| target_work_id.is_empty()
|
||||
{
|
||||
return Err("拼图 remix 参数不能为空".to_string());
|
||||
}
|
||||
if input.author_display_name.trim().is_empty() {
|
||||
return Err("拼图 remix 作者名不能为空".to_string());
|
||||
}
|
||||
ensure_session_missing(ctx, target_session_id)?;
|
||||
ensure_message_missing(ctx, input.welcome_message_id.trim())?;
|
||||
if ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&target_profile_id.to_string())
|
||||
.is_some()
|
||||
{
|
||||
return Err("拼图 remix 目标作品已存在".to_string());
|
||||
}
|
||||
|
||||
let source = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&source_profile_id.to_string())
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.ok_or_else(|| "拼图已发布源作品不存在".to_string())?;
|
||||
let source_profile = build_puzzle_work_profile_from_row(&source)?;
|
||||
let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros);
|
||||
|
||||
replace_puzzle_work_profile(
|
||||
ctx,
|
||||
&source,
|
||||
PuzzleWorkProfileRow {
|
||||
profile_id: source.profile_id.clone(),
|
||||
work_id: source.work_id.clone(),
|
||||
owner_user_id: source.owner_user_id.clone(),
|
||||
source_session_id: source.source_session_id.clone(),
|
||||
author_display_name: source.author_display_name.clone(),
|
||||
level_name: source.level_name.clone(),
|
||||
summary: source.summary.clone(),
|
||||
theme_tags_json: source.theme_tags_json.clone(),
|
||||
cover_image_src: source.cover_image_src.clone(),
|
||||
cover_asset_id: source.cover_asset_id.clone(),
|
||||
publication_status: source.publication_status,
|
||||
play_count: source.play_count,
|
||||
remix_count: source.remix_count.saturating_add(1),
|
||||
like_count: source.like_count,
|
||||
anchor_pack_json: source.anchor_pack_json.clone(),
|
||||
publish_ready: source.publish_ready,
|
||||
created_at: source.created_at,
|
||||
updated_at: remixed_at,
|
||||
published_at: source.published_at,
|
||||
},
|
||||
);
|
||||
|
||||
let draft = PuzzleResultDraft {
|
||||
level_name: source_profile.level_name.clone(),
|
||||
summary: source_profile.summary.clone(),
|
||||
theme_tags: source_profile.theme_tags.clone(),
|
||||
forbidden_directives: Vec::new(),
|
||||
creator_intent: None,
|
||||
anchor_pack: source_profile.anchor_pack.clone(),
|
||||
candidates: Vec::new(),
|
||||
selected_candidate_id: None,
|
||||
cover_image_src: source_profile.cover_image_src.clone(),
|
||||
cover_asset_id: source_profile.cover_asset_id.clone(),
|
||||
generation_status: "ready".to_string(),
|
||||
};
|
||||
ctx.db.puzzle_agent_session().insert(PuzzleAgentSessionRow {
|
||||
session_id: target_session_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
seed_text: source_profile.summary.clone(),
|
||||
current_turn: 1,
|
||||
progress_percent: 88,
|
||||
stage: PuzzleAgentStage::DraftReady,
|
||||
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
|
||||
draft_json: Some(serialize_json(&draft)),
|
||||
last_assistant_reply: Some("已从公开作品 Remix 出新的拼图草稿。".to_string()),
|
||||
published_profile_id: None,
|
||||
created_at: remixed_at,
|
||||
updated_at: remixed_at,
|
||||
});
|
||||
ctx.db.puzzle_agent_message().insert(PuzzleAgentMessageRow {
|
||||
message_id: input.welcome_message_id,
|
||||
session_id: target_session_id.to_string(),
|
||||
role: PuzzleAgentMessageRole::Assistant,
|
||||
kind: PuzzleAgentMessageKind::Summary,
|
||||
text: "已复制公开作品为你的草稿。".to_string(),
|
||||
created_at: remixed_at,
|
||||
});
|
||||
ctx.db.puzzle_work_profile().insert(PuzzleWorkProfileRow {
|
||||
profile_id: target_profile_id.to_string(),
|
||||
work_id: target_work_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
source_session_id: Some(target_session_id.to_string()),
|
||||
author_display_name: input.author_display_name.trim().to_string(),
|
||||
level_name: source_profile.level_name,
|
||||
summary: source_profile.summary,
|
||||
theme_tags_json: serialize_json(&source_profile.theme_tags),
|
||||
cover_image_src: source_profile.cover_image_src,
|
||||
cover_asset_id: source_profile.cover_asset_id,
|
||||
publication_status: PuzzlePublicationStatus::Draft,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
|
||||
publish_ready: true,
|
||||
created_at: remixed_at,
|
||||
updated_at: remixed_at,
|
||||
published_at: None,
|
||||
});
|
||||
|
||||
get_puzzle_agent_session_tx(
|
||||
ctx,
|
||||
PuzzleAgentSessionGetInput {
|
||||
session_id: target_session_id.to_string(),
|
||||
owner_user_id: target_owner_user_id.to_string(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn start_puzzle_run_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunStartInput,
|
||||
@@ -1308,6 +1465,8 @@ fn build_puzzle_work_profile_from_row(
|
||||
.published_at
|
||||
.map(|value| value.to_micros_since_unix_epoch()),
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
publish_ready: row.publish_ready,
|
||||
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
|
||||
})
|
||||
@@ -1507,6 +1666,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
// 二次编辑发布同一个 profile 时,作品内容可以覆盖,但历史游玩数属于
|
||||
// 广场消费数据,不能因为重新发布被清零。
|
||||
play_count: existing.play_count.max(profile.play_count),
|
||||
remix_count: existing.remix_count.max(profile.remix_count),
|
||||
like_count: existing.like_count.max(profile.like_count),
|
||||
anchor_pack_json: serialize_json(&profile.anchor_pack),
|
||||
publish_ready: profile.publish_ready,
|
||||
created_at: existing.created_at,
|
||||
@@ -1532,6 +1693,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
cover_asset_id: profile.cover_asset_id,
|
||||
publication_status: profile.publication_status,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
like_count: profile.like_count,
|
||||
anchor_pack_json: serialize_json(&profile.anchor_pack),
|
||||
publish_ready: profile.publish_ready,
|
||||
created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros),
|
||||
@@ -1620,6 +1783,8 @@ fn increment_puzzle_profile_play_count(
|
||||
cover_asset_id: row.cover_asset_id.clone(),
|
||||
publication_status: row.publication_status,
|
||||
play_count: row.play_count.saturating_add(1),
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
created_at: row.created_at,
|
||||
|
||||
Reference in New Issue
Block a user