1
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use crate::runtime::{
|
||||
ProfilePlayedWorkUpsertInput, add_profile_observed_play_time, upsert_profile_played_work,
|
||||
ProfilePlayedWorkUpsertInput, PublicWorkPlayRecordInput, add_profile_observed_play_time,
|
||||
count_recent_public_work_plays, record_public_work_play, upsert_profile_played_work,
|
||||
};
|
||||
use module_puzzle::{
|
||||
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
||||
@@ -8,13 +9,14 @@ use module_puzzle::{
|
||||
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
|
||||
PuzzleGeneratedImagesSaveInput, PuzzleLeaderboardEntry, PuzzleLeaderboardSubmitInput,
|
||||
PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft, PuzzleRunDragInput,
|
||||
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult, PuzzleRunSnapshot,
|
||||
PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput,
|
||||
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
|
||||
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult,
|
||||
PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
|
||||
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput,
|
||||
PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile, 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,
|
||||
publish_work_profile, resolve_puzzle_grid_size, select_next_profile,
|
||||
};
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::to_string as json_to_string;
|
||||
@@ -77,13 +79,15 @@ 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,
|
||||
updated_at: Timestamp,
|
||||
published_at: Option<Timestamp>,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
}
|
||||
|
||||
/// 运行态 run 快照表。
|
||||
@@ -503,6 +507,44 @@ pub fn advance_puzzle_next_level(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn update_puzzle_run_pause(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleRunPauseInput,
|
||||
) -> PuzzleRunProcedureResult {
|
||||
match ctx.try_with_tx(|tx| update_puzzle_run_pause_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn use_puzzle_runtime_prop(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleRunPropInput,
|
||||
) -> PuzzleRunProcedureResult {
|
||||
match ctx.try_with_tx(|tx| use_puzzle_runtime_prop_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_puzzle_leaderboard_entry(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -755,7 +797,7 @@ fn save_puzzle_generated_images_tx(
|
||||
}
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready {
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥主")).publish_ready {
|
||||
PuzzleAgentStage::ReadyToPublish
|
||||
} else {
|
||||
PuzzleAgentStage::ImageRefining
|
||||
@@ -804,7 +846,7 @@ fn select_puzzle_cover_image_tx(
|
||||
let draft =
|
||||
apply_selected_candidate(draft, &input.candidate_id).map_err(|error| error.to_string())?;
|
||||
let selected_at = Timestamp::from_micros_since_unix_epoch(input.selected_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("创作者")).publish_ready {
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥主")).publish_ready {
|
||||
PuzzleAgentStage::ReadyToPublish
|
||||
} else {
|
||||
PuzzleAgentStage::ImageRefining
|
||||
@@ -1029,12 +1071,13 @@ fn delete_puzzle_work_tx(
|
||||
}
|
||||
|
||||
fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.map(|row| build_puzzle_work_profile_from_row(&row))
|
||||
.map(|row| build_puzzle_work_profile_from_row_with_recent_count(ctx, &row, now_micros))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
Ok(items)
|
||||
@@ -1053,7 +1096,11 @@ fn get_puzzle_gallery_detail_tx(
|
||||
if row.publication_status != PuzzlePublicationStatus::Published {
|
||||
return Err("拼图作品尚未发布".to_string());
|
||||
}
|
||||
build_puzzle_work_profile_from_row(&row)
|
||||
build_puzzle_work_profile_from_row_with_recent_count(
|
||||
ctx,
|
||||
&row,
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
)
|
||||
}
|
||||
|
||||
fn remix_puzzle_work_tx(
|
||||
@@ -1213,8 +1260,9 @@ fn start_puzzle_run_tx(
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
let mut run =
|
||||
start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
|
||||
let started_at_ms = micros_to_millis(input.started_at_micros);
|
||||
let mut run = module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let current_grid_size = run.current_grid_size;
|
||||
let current_profile_id = entry_profile.profile_id.clone();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1231,6 +1279,15 @@ fn start_puzzle_run_tx(
|
||||
)
|
||||
.map(|value| value.profile_id.clone());
|
||||
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
source_type: "puzzle".to_string(),
|
||||
owner_user_id: entry_profile_row.owner_user_id.clone(),
|
||||
profile_id: entry_profile_row.profile_id.clone(),
|
||||
played_at_micros: input.started_at_micros,
|
||||
},
|
||||
)?;
|
||||
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
@@ -1247,7 +1304,14 @@ fn get_puzzle_run_tx(
|
||||
input: PuzzleRunGetInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let mut run = deserialize_run(&row.snapshot_json)?;
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
let mut run = module_puzzle::resolve_puzzle_run_timer_at(
|
||||
deserialize_run(&row.snapshot_json)?,
|
||||
micros_to_millis(now_micros),
|
||||
);
|
||||
if serialize_json(&run) != row.snapshot_json {
|
||||
replace_puzzle_runtime_run(ctx, &row, &run, now_micros);
|
||||
}
|
||||
if let Some((profile_id, grid_size)) = run
|
||||
.current_level
|
||||
.as_ref()
|
||||
@@ -1270,9 +1334,27 @@ fn swap_puzzle_pieces_tx(
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let mut next_run = swap_pieces(¤t_run, &input.first_piece_id, &input.second_piece_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut next_run = module_puzzle::swap_pieces_at(
|
||||
¤t_run,
|
||||
&input.first_piece_id,
|
||||
&input.second_piece_id,
|
||||
micros_to_millis(input.swapped_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
||||
if let Some((profile_id, grid_size)) = next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut next_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.swapped_at_micros);
|
||||
Ok(next_run)
|
||||
}
|
||||
@@ -1283,14 +1365,28 @@ fn drag_puzzle_piece_or_group_tx(
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let mut next_run = module_puzzle::drag_piece_or_group(
|
||||
let mut next_run = module_puzzle::drag_piece_or_group_at(
|
||||
¤t_run,
|
||||
&input.piece_id,
|
||||
input.target_row,
|
||||
input.target_col,
|
||||
micros_to_millis(input.dragged_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
||||
if let Some((profile_id, grid_size)) = next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut next_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.dragged_at_micros);
|
||||
Ok(next_run)
|
||||
}
|
||||
@@ -1323,8 +1419,12 @@ fn advance_puzzle_next_level_tx(
|
||||
)
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
||||
.clone();
|
||||
let mut next_run = module_puzzle::advance_next_level(¤t_run, &next_profile)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut next_run = module_puzzle::advance_next_level_at(
|
||||
¤t_run,
|
||||
&next_profile,
|
||||
micros_to_millis(input.advanced_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let next_grid_size = next_run.current_grid_size;
|
||||
let next_profile_id = next_profile.profile_id.clone();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1344,6 +1444,15 @@ fn advance_puzzle_next_level_tx(
|
||||
.profile_id()
|
||||
.find(&next_profile.profile_id)
|
||||
{
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
source_type: "puzzle".to_string(),
|
||||
owner_user_id: next_profile_row.owner_user_id.clone(),
|
||||
profile_id: next_profile_row.profile_id.clone(),
|
||||
played_at_micros: input.advanced_at_micros,
|
||||
},
|
||||
)?;
|
||||
increment_puzzle_profile_play_count(ctx, &next_profile_row, input.advanced_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
@@ -1356,6 +1465,82 @@ fn advance_puzzle_next_level_tx(
|
||||
Ok(next_run)
|
||||
}
|
||||
|
||||
fn update_puzzle_run_pause_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunPauseInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let next_run = module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
input.paused,
|
||||
micros_to_millis(input.updated_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.updated_at_micros);
|
||||
let mut hydrated_run = next_run;
|
||||
if let Some((profile_id, grid_size)) = hydrated_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut hydrated_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
Ok(hydrated_run)
|
||||
}
|
||||
|
||||
fn use_puzzle_runtime_prop_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunPropInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let next_run = match input.prop_kind.as_str() {
|
||||
"freezeTime" | "freeze_time" => {
|
||||
module_puzzle::apply_puzzle_freeze_time_at(
|
||||
¤t_run,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
"hint" => module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
false,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
"reference" => module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
true,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
_ => return Err("未知拼图道具".to_string()),
|
||||
};
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.used_at_micros);
|
||||
let mut hydrated_run = next_run;
|
||||
if let Some((profile_id, grid_size)) = hydrated_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut hydrated_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
Ok(hydrated_run)
|
||||
}
|
||||
|
||||
fn submit_puzzle_leaderboard_entry_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleLeaderboardSubmitInput,
|
||||
@@ -1424,7 +1609,7 @@ fn build_puzzle_agent_session_snapshot(
|
||||
let messages = list_session_messages(ctx, &row.session_id);
|
||||
let result_preview = draft
|
||||
.as_ref()
|
||||
.map(|value| build_result_preview(value, Some("创作者")));
|
||||
.map(|value| build_result_preview(value, Some("陶泥主")));
|
||||
|
||||
Ok(PuzzleAgentSessionSnapshot {
|
||||
session_id: row.session_id.clone(),
|
||||
@@ -1447,6 +1632,23 @@ fn build_puzzle_agent_session_snapshot(
|
||||
|
||||
fn build_puzzle_work_profile_from_row(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
build_puzzle_work_profile_from_row_without_recent_count(row)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_with_recent_count(
|
||||
ctx: &TxContext,
|
||||
row: &PuzzleWorkProfileRow,
|
||||
now_micros: i64,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
let mut profile = build_puzzle_work_profile_from_row_without_recent_count(row)?;
|
||||
profile.recent_play_count_7d =
|
||||
count_recent_public_work_plays(ctx, "puzzle", &row.profile_id, now_micros);
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_without_recent_count(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
Ok(PuzzleWorkProfile {
|
||||
work_id: row.work_id.clone(),
|
||||
@@ -1467,6 +1669,7 @@ fn build_puzzle_work_profile_from_row(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
publish_ready: row.publish_ready,
|
||||
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
|
||||
})
|
||||
@@ -1482,6 +1685,13 @@ fn build_puzzle_work_ids_from_session_id(session_id: &str) -> (String, String) {
|
||||
)
|
||||
}
|
||||
|
||||
fn micros_to_millis(value: i64) -> u64 {
|
||||
if value <= 0 {
|
||||
return 0;
|
||||
}
|
||||
(value as u64).saturating_div(1_000)
|
||||
}
|
||||
|
||||
fn upsert_puzzle_draft_work_profile(
|
||||
ctx: &TxContext,
|
||||
session_id: &str,
|
||||
@@ -1500,7 +1710,7 @@ fn upsert_puzzle_draft_work_profile(
|
||||
profile_id,
|
||||
owner_user_id.to_string(),
|
||||
Some(session_id.to_string()),
|
||||
"创作者".to_string(),
|
||||
"陶泥主".to_string(),
|
||||
draft,
|
||||
updated_at_micros,
|
||||
)
|
||||
@@ -2095,6 +2305,9 @@ mod tests {
|
||||
updated_at_micros: 1,
|
||||
published_at_micros: Some(1),
|
||||
play_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
};
|
||||
@@ -2110,6 +2323,9 @@ mod tests {
|
||||
updated_at_micros: 2,
|
||||
published_at_micros: Some(2),
|
||||
play_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
source_session_id: None,
|
||||
|
||||
Reference in New Issue
Block a user