1
This commit is contained in:
@@ -35,7 +35,7 @@ use crate::{
|
||||
big_fish::{
|
||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
|
||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, record_big_fish_play,
|
||||
stream_big_fish_message, submit_big_fish_message,
|
||||
remix_big_fish_gallery_work, stream_big_fish_message, submit_big_fish_message,
|
||||
},
|
||||
character_animation_assets::{
|
||||
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
||||
@@ -56,6 +56,7 @@ use crate::{
|
||||
get_custom_world_gallery_detail_by_code, get_custom_world_library,
|
||||
get_custom_world_library_detail, get_custom_world_works, list_custom_world_gallery,
|
||||
publish_custom_world_library_profile, put_custom_world_library_profile,
|
||||
record_custom_world_gallery_play, remix_custom_world_gallery_profile,
|
||||
stream_custom_world_agent_message, submit_custom_world_agent_message,
|
||||
unpublish_custom_world_library_profile,
|
||||
},
|
||||
@@ -84,8 +85,8 @@ use crate::{
|
||||
delete_puzzle_work, drag_puzzle_piece_or_group, execute_puzzle_agent_action,
|
||||
get_puzzle_agent_session, get_puzzle_gallery_detail, get_puzzle_run,
|
||||
get_puzzle_work_detail, get_puzzle_works, list_puzzle_gallery, put_puzzle_work,
|
||||
start_puzzle_run, stream_puzzle_agent_message, submit_puzzle_agent_message,
|
||||
submit_puzzle_leaderboard, swap_puzzle_pieces,
|
||||
remix_puzzle_gallery_work, start_puzzle_run, stream_puzzle_agent_message,
|
||||
submit_puzzle_agent_message, submit_puzzle_leaderboard, swap_puzzle_pieces,
|
||||
},
|
||||
refresh_session::refresh_session,
|
||||
request_context::{attach_request_context, resolve_request_id},
|
||||
@@ -522,6 +523,20 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}",
|
||||
get(get_custom_world_gallery_detail),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/remix",
|
||||
post(remix_custom_world_gallery_profile).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/custom-world-gallery/{owner_user_id}/{profile_id}/play",
|
||||
post(record_custom_world_gallery_play).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/custom-world-gallery/by-code/{code}",
|
||||
get(get_custom_world_gallery_detail_by_code),
|
||||
@@ -634,6 +649,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
)),
|
||||
)
|
||||
.route("/api/runtime/big-fish/gallery", get(list_big_fish_gallery))
|
||||
.route(
|
||||
"/api/runtime/big-fish/gallery/{session_id}/remix",
|
||||
post(remix_big_fish_gallery_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/works/{session_id}",
|
||||
delete(delete_big_fish_work).route_layer(middleware::from_fn_with_state(
|
||||
@@ -712,6 +734,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
"/api/runtime/puzzle/gallery/{profile_id}",
|
||||
get(get_puzzle_gallery_detail),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/gallery/{profile_id}/remix",
|
||||
post(remix_puzzle_gallery_work).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/runs",
|
||||
post(start_puzzle_run).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -35,7 +35,7 @@ use spacetime_client::{
|
||||
BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput, BigFishGameDraftRecord,
|
||||
BigFishLevelBlueprintRecord, BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput,
|
||||
BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord,
|
||||
BigFishWorkSummaryRecord, SpacetimeClientError,
|
||||
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, SpacetimeClientError,
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -251,6 +251,36 @@ pub async fn record_big_fish_play(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn remix_big_fish_gallery_work(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
||||
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.remix_big_fish_work(BigFishWorkRemixRecordInput {
|
||||
source_session_id: session_id,
|
||||
target_session_id: build_prefixed_uuid_id("big-fish-session-"),
|
||||
target_owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
welcome_message_id: build_prefixed_uuid_id("big-fish-message-"),
|
||||
remixed_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
BigFishSessionResponse {
|
||||
session: map_big_fish_session_response(session),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn submit_big_fish_message(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
@@ -906,12 +936,15 @@ fn map_big_fish_work_summary_response(
|
||||
cover_image_src: item.cover_image_src,
|
||||
status: item.status,
|
||||
updated_at: current_timestamp_micros_to_string(item.updated_at_micros),
|
||||
published_at: item.published_at_micros.map(current_timestamp_micros_to_string),
|
||||
publish_ready: item.publish_ready,
|
||||
level_count: item.level_count,
|
||||
level_main_image_ready_count: item.level_main_image_ready_count,
|
||||
level_motion_ready_count: item.level_motion_ready_count,
|
||||
background_ready: item.background_ready,
|
||||
play_count: item.play_count,
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ use spacetime_client::{
|
||||
CustomWorldAgentSessionRecord, CustomWorldDraftCardDetailRecord,
|
||||
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
|
||||
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord,
|
||||
CustomWorldProfilePlayReportRecordInput, CustomWorldProfileRemixRecordInput,
|
||||
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
|
||||
CustomWorldResultPreviewBlockerRecord, CustomWorldSupportedActionRecord,
|
||||
CustomWorldWorkSummaryRecord, SpacetimeClientError,
|
||||
@@ -758,6 +759,82 @@ pub async fn get_custom_world_gallery_detail_by_code(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn remix_custom_world_gallery_profile(
|
||||
State(state): State<AppState>,
|
||||
Path((owner_user_id, profile_id)): Path<(String, String)>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
if owner_user_id.trim().is_empty() || profile_id.trim().is_empty() {
|
||||
return Err(custom_world_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "custom-world-gallery",
|
||||
"message": "ownerUserId and profileId are required",
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let mutation = state
|
||||
.spacetime_client()
|
||||
.remix_custom_world_profile(CustomWorldProfileRemixRecordInput {
|
||||
source_owner_user_id: owner_user_id,
|
||||
source_profile_id: profile_id,
|
||||
target_owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
target_profile_id: build_prefixed_uuid_id("custom-world-profile-"),
|
||||
author_display_name: resolve_author_display_name(&state, &authenticated),
|
||||
remixed_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
CustomWorldLibraryMutationResponse {
|
||||
entry: map_custom_world_library_entry_response(mutation.entry.clone()),
|
||||
entries: vec![map_custom_world_library_entry_response(mutation.entry)],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn record_custom_world_gallery_play(
|
||||
State(state): State<AppState>,
|
||||
Path((owner_user_id, profile_id)): Path<(String, String)>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
if owner_user_id.trim().is_empty() || profile_id.trim().is_empty() {
|
||||
return Err(custom_world_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "custom-world-gallery",
|
||||
"message": "ownerUserId and profileId are required",
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let mutation = state
|
||||
.spacetime_client()
|
||||
.record_custom_world_profile_play(CustomWorldProfilePlayReportRecordInput {
|
||||
owner_user_id,
|
||||
profile_id,
|
||||
played_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
CustomWorldGalleryDetailResponse {
|
||||
entry: map_custom_world_library_entry_response(mutation.entry),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn create_custom_world_agent_session(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -2632,6 +2709,9 @@ fn map_custom_world_library_entry_response(
|
||||
theme_mode: entry.theme_mode,
|
||||
playable_npc_count: entry.playable_npc_count,
|
||||
landmark_count: entry.landmark_count,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2654,6 +2734,9 @@ fn map_custom_world_gallery_card_response(
|
||||
theme_mode: entry.theme_mode,
|
||||
playable_npc_count: entry.playable_npc_count,
|
||||
landmark_count: entry.landmark_count,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ use spacetime_client::{
|
||||
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
|
||||
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
|
||||
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, SpacetimeClientError,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
use tokio::time::sleep;
|
||||
@@ -882,6 +882,49 @@ pub async fn get_puzzle_gallery_detail(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn remix_puzzle_gallery_work(
|
||||
State(state): State<AppState>,
|
||||
AxumPath(profile_id): AxumPath<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(
|
||||
&request_context,
|
||||
PUZZLE_GALLERY_PROVIDER,
|
||||
&profile_id,
|
||||
"profileId",
|
||||
)?;
|
||||
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.remix_puzzle_work(PuzzleWorkRemixRecordInput {
|
||||
source_profile_id: profile_id,
|
||||
target_owner_user_id: owner_user_id,
|
||||
target_session_id: build_prefixed_uuid_id("puzzle-session-"),
|
||||
target_profile_id: build_prefixed_uuid_id("puzzle-profile-"),
|
||||
target_work_id: build_prefixed_uuid_id("puzzle-work-"),
|
||||
author_display_name: resolve_author_display_name(&state, &authenticated),
|
||||
welcome_message_id: build_prefixed_uuid_id("puzzle-message-"),
|
||||
remixed_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_GALLERY_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PuzzleAgentSessionResponse {
|
||||
session: map_puzzle_agent_session_response(session),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn start_puzzle_run(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -1354,6 +1397,8 @@ fn map_puzzle_work_summary_response(item: PuzzleWorkProfileRecord) -> PuzzleWork
|
||||
updated_at: item.updated_at,
|
||||
published_at: item.published_at,
|
||||
play_count: item.play_count,
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
publish_ready: item.publish_ready,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use axum::{
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use module_runtime_story_compat::{
|
||||
@@ -38,6 +39,8 @@ pub struct NpcChatTurnRequest {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
#[serde(default)]
|
||||
world_type: String,
|
||||
#[serde(default)]
|
||||
character: Option<Value>,
|
||||
@@ -292,6 +295,16 @@ async fn hydrate_npc_chat_turn_request_from_session(
|
||||
// 中文注释:旧调用没有 sessionId 时继续使用请求体字段;正式运行态由后端快照投影上下文。
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(game_state) = resolve_request_snapshot_game_state(
|
||||
request_context,
|
||||
session_id.as_str(),
|
||||
payload.snapshot.as_ref(),
|
||||
)? {
|
||||
apply_npc_chat_turn_game_state(payload, game_state);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let record = state
|
||||
.get_runtime_snapshot_record(user_id)
|
||||
.await
|
||||
@@ -328,6 +341,49 @@ async fn hydrate_npc_chat_turn_request_from_session(
|
||||
));
|
||||
}
|
||||
|
||||
apply_npc_chat_turn_game_state(payload, game_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_request_snapshot_game_state(
|
||||
request_context: &RequestContext,
|
||||
session_id: &str,
|
||||
snapshot: Option<&RuntimeStorySnapshotPayload>,
|
||||
) -> Result<Option<Value>, Response> {
|
||||
let Some(snapshot) = snapshot else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !snapshot.game_state.is_object() {
|
||||
return Err(runtime_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"field": "snapshot.gameState",
|
||||
"message": "snapshot.gameState 必须是 JSON object",
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let snapshot_session_id =
|
||||
read_runtime_session_id(&snapshot.game_state).unwrap_or_else(|| session_id.to_string());
|
||||
if snapshot_session_id != session_id {
|
||||
return Err(runtime_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"message": "请求的运行时会话与服务端快照不一致,请重新进入游戏",
|
||||
"sessionId": session_id,
|
||||
"snapshotSessionId": snapshot_session_id,
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
// 中文注释:预览/测试/禁存运行态只把请求 snapshot 用于本轮 prompt 投影,不写入正式存档。
|
||||
Ok(Some(snapshot.game_state.clone()))
|
||||
}
|
||||
|
||||
fn apply_npc_chat_turn_game_state(payload: &mut NpcChatTurnRequest, game_state: Value) {
|
||||
payload.world_type = current_world_type(&game_state).unwrap_or_default();
|
||||
payload.character = read_field(&game_state, "playerCharacter").cloned();
|
||||
payload.player = payload.character.clone();
|
||||
@@ -361,8 +417,6 @@ async fn hydrate_npc_chat_turn_request_from_session(
|
||||
object.insert("state".to_string(), game_state);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_current_request_npc_state(game_state: &Value) -> Option<Value> {
|
||||
@@ -709,6 +763,8 @@ fn runtime_chat_error_response(request_context: &RequestContext, error: AppError
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{config::AppConfig, request_context::RequestContext, state::AppState};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn npc_chat_affinity_delta_keeps_node_keyword_rules() {
|
||||
@@ -752,4 +808,129 @@ mod tests {
|
||||
vec!["继续问线索", "表明立场", "拉近关系"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn npc_chat_turn_prefers_request_snapshot_over_persisted_session() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
state
|
||||
.put_runtime_snapshot_record(
|
||||
"user_00000001".to_string(),
|
||||
1,
|
||||
"adventure".to_string(),
|
||||
json!({
|
||||
"worldType": "WUXIA",
|
||||
"runtimeSessionId": "runtime-main",
|
||||
"playerCharacter": { "id": "hero-main", "name": "旧存档" },
|
||||
"currentEncounter": { "id": "npc-main", "npcName": "旧 NPC" },
|
||||
"sceneHostileNpcs": [],
|
||||
"storyHistory": [],
|
||||
}),
|
||||
None,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.expect("snapshot should seed");
|
||||
let request_context = test_request_context();
|
||||
let mut payload = test_npc_chat_turn_payload(
|
||||
"runtime-preview",
|
||||
Some(json!({
|
||||
"worldType": "CUSTOM",
|
||||
"runtimeSessionId": "runtime-preview",
|
||||
"runtimePersistenceDisabled": true,
|
||||
"playerCharacter": { "id": "hero-preview", "name": "临时角色" },
|
||||
"currentEncounter": { "id": "npc-preview", "npcName": "临时 NPC" },
|
||||
"sceneHostileNpcs": [{ "id": "monster-preview", "name": "雾影" }],
|
||||
"storyHistory": [{ "text": "临时故事" }],
|
||||
"npcStates": {
|
||||
"npc-preview": {
|
||||
"affinity": 12,
|
||||
"helpUsed": false,
|
||||
"chattedCount": 2,
|
||||
"giftsGiven": 0,
|
||||
"recruited": false
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
hydrate_npc_chat_turn_request_from_session(
|
||||
&state,
|
||||
&request_context,
|
||||
"user_00000001".to_string(),
|
||||
&mut payload,
|
||||
)
|
||||
.await
|
||||
.expect("request snapshot should hydrate");
|
||||
|
||||
assert_eq!(payload.world_type, "CUSTOM");
|
||||
assert_eq!(
|
||||
read_optional_string_field(&payload.encounter, "npcName").as_deref(),
|
||||
Some("临时 NPC")
|
||||
);
|
||||
assert_eq!(payload.monsters.len(), 1);
|
||||
assert_eq!(read_i32_field(&payload.npc_state, "affinity"), Some(12));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn npc_chat_turn_rejects_request_snapshot_session_mismatch() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
let request_context = test_request_context();
|
||||
let mut payload = test_npc_chat_turn_payload(
|
||||
"runtime-preview",
|
||||
Some(json!({
|
||||
"worldType": "WUXIA",
|
||||
"runtimeSessionId": "runtime-other",
|
||||
})),
|
||||
);
|
||||
|
||||
let response = hydrate_npc_chat_turn_request_from_session(
|
||||
&state,
|
||||
&request_context,
|
||||
"user_00000001".to_string(),
|
||||
&mut payload,
|
||||
)
|
||||
.await
|
||||
.expect_err("snapshot session mismatch should fail");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::CONFLICT);
|
||||
}
|
||||
|
||||
fn test_request_context() -> RequestContext {
|
||||
RequestContext::new(
|
||||
"runtime-chat-test".to_string(),
|
||||
"POST /api/runtime/chat/npc/turn/stream".to_string(),
|
||||
Duration::ZERO,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_npc_chat_turn_payload(
|
||||
session_id: &str,
|
||||
game_state: Option<Value>,
|
||||
) -> NpcChatTurnRequest {
|
||||
NpcChatTurnRequest {
|
||||
session_id: Some(session_id.to_string()),
|
||||
snapshot: game_state.map(|game_state| RuntimeStorySnapshotPayload {
|
||||
saved_at: None,
|
||||
bottom_tab: "adventure".to_string(),
|
||||
game_state,
|
||||
current_story: None,
|
||||
}),
|
||||
world_type: String::new(),
|
||||
character: None,
|
||||
player: None,
|
||||
encounter: json!({ "id": "npc-request", "npcName": "请求 NPC" }),
|
||||
monsters: Vec::new(),
|
||||
history: Vec::new(),
|
||||
context: Value::Null,
|
||||
conversation_history: Vec::new(),
|
||||
dialogue: Vec::new(),
|
||||
combat_context: None,
|
||||
player_message: "你刚才看见了什么?".to_string(),
|
||||
npc_state: Value::Null,
|
||||
npc_initiates_conversation: false,
|
||||
quest_offer_context: None,
|
||||
chat_directive: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use axum::{
|
||||
use platform_llm::{LlmMessage, LlmTextRequest};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime_story::RuntimeStorySnapshotPayload;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use crate::{
|
||||
@@ -27,6 +28,8 @@ pub struct RuntimeCharacterChatRequest {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
#[serde(default)]
|
||||
world_type: String,
|
||||
#[serde(default)]
|
||||
player_character: Value,
|
||||
@@ -54,6 +57,8 @@ pub struct RuntimeNpcDialogueRequest {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
#[serde(default)]
|
||||
world_type: String,
|
||||
#[serde(default)]
|
||||
character: Value,
|
||||
@@ -77,6 +82,8 @@ pub struct RuntimeNpcRecruitDialogueRequest {
|
||||
#[serde(default)]
|
||||
session_id: Option<String>,
|
||||
#[serde(default)]
|
||||
snapshot: Option<RuntimeStorySnapshotPayload>,
|
||||
#[serde(default)]
|
||||
world_type: String,
|
||||
#[serde(default)]
|
||||
character: Value,
|
||||
@@ -346,6 +353,7 @@ async fn hydrate_character_chat_request_from_session(
|
||||
request_context,
|
||||
user_id,
|
||||
payload.session_id.as_deref(),
|
||||
payload.snapshot.as_ref(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -382,6 +390,7 @@ async fn hydrate_npc_dialogue_request_from_session(
|
||||
request_context,
|
||||
user_id,
|
||||
payload.session_id.as_deref(),
|
||||
payload.snapshot.as_ref(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -430,6 +439,7 @@ async fn hydrate_npc_recruit_request_from_session(
|
||||
request_context,
|
||||
user_id,
|
||||
payload.session_id.as_deref(),
|
||||
payload.snapshot.as_ref(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
@@ -472,11 +482,19 @@ async fn resolve_runtime_chat_game_state(
|
||||
request_context: &RequestContext,
|
||||
user_id: String,
|
||||
session_id: Option<&str>,
|
||||
snapshot: Option<&RuntimeStorySnapshotPayload>,
|
||||
) -> Result<Option<Value>, Response> {
|
||||
let Some(session_id) = session_id.and_then(normalize_required_string) else {
|
||||
// 中文注释:未携带 sessionId 的旧调用仅保留兼容,后续正式运行态应全部走后端快照。
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Some(game_state) =
|
||||
resolve_request_snapshot_game_state(request_context, session_id.as_str(), snapshot)?
|
||||
{
|
||||
return Ok(Some(game_state));
|
||||
}
|
||||
|
||||
let record = state
|
||||
.get_runtime_snapshot_record(user_id)
|
||||
.await
|
||||
@@ -516,6 +534,43 @@ async fn resolve_runtime_chat_game_state(
|
||||
Ok(Some(game_state))
|
||||
}
|
||||
|
||||
fn resolve_request_snapshot_game_state(
|
||||
request_context: &RequestContext,
|
||||
session_id: &str,
|
||||
snapshot: Option<&RuntimeStorySnapshotPayload>,
|
||||
) -> Result<Option<Value>, Response> {
|
||||
let Some(snapshot) = snapshot else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !snapshot.game_state.is_object() {
|
||||
return Err(runtime_plain_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"field": "snapshot.gameState",
|
||||
"message": "snapshot.gameState 必须是 JSON object",
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let snapshot_session_id =
|
||||
read_runtime_session_id(&snapshot.game_state).unwrap_or_else(|| session_id.to_string());
|
||||
if snapshot_session_id != session_id {
|
||||
return Err(runtime_plain_chat_error_response(
|
||||
request_context,
|
||||
AppError::from_status(StatusCode::CONFLICT).with_details(json!({
|
||||
"provider": "runtime-chat",
|
||||
"message": "请求的运行时会话与服务端快照不一致,请重新进入游戏",
|
||||
"sessionId": session_id,
|
||||
"snapshotSessionId": snapshot_session_id,
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
// 中文注释:临时运行态聊天只读取请求 snapshot 构造上下文,不把它写回 runtime_snapshot。
|
||||
Ok(Some(snapshot.game_state.clone()))
|
||||
}
|
||||
|
||||
async fn request_runtime_plain_text(
|
||||
state: &AppState,
|
||||
system_prompt: &'static str,
|
||||
|
||||
@@ -226,6 +226,9 @@ pub struct BigFishWorkSummarySnapshot {
|
||||
pub level_motion_ready_count: u32,
|
||||
pub background_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub published_at_micros: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -242,6 +245,16 @@ pub struct BigFishWorkDeleteInput {
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorkRemixInput {
|
||||
pub source_session_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BigFishWorksProcedureResult {
|
||||
|
||||
@@ -185,6 +185,9 @@ pub struct CustomWorldProfileSnapshot {
|
||||
pub profile_payload_json: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub author_display_name: String,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub deleted_at_micros: Option<i64>,
|
||||
@@ -207,6 +210,9 @@ pub struct CustomWorldGalleryEntrySnapshot {
|
||||
pub theme_mode: CustomWorldThemeMode,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub published_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
@@ -484,6 +490,25 @@ pub struct CustomWorldGalleryDetailByCodeInput {
|
||||
pub public_work_code: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CustomWorldProfileRemixInput {
|
||||
pub source_owner_user_id: String,
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CustomWorldProfilePlayRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CustomWorldAgentSessionCreateInput {
|
||||
|
||||
@@ -208,7 +208,12 @@ pub struct PuzzleWorkProfile {
|
||||
pub publication_status: PuzzlePublicationStatus,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub play_count: u32,
|
||||
#[serde(default)]
|
||||
pub remix_count: u32,
|
||||
#[serde(default)]
|
||||
pub like_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub anchor_pack: PuzzleAnchorPack,
|
||||
}
|
||||
@@ -407,6 +412,19 @@ pub struct PuzzleWorkUpsertInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleWorkRemixInput {
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub target_work_id: String,
|
||||
pub author_display_name: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleRunStartInput {
|
||||
@@ -846,6 +864,8 @@ pub fn create_work_profile(
|
||||
updated_at_micros,
|
||||
published_at_micros: None,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: preview.publish_ready,
|
||||
anchor_pack: draft.anchor_pack.clone(),
|
||||
})
|
||||
@@ -2015,6 +2035,8 @@ mod tests {
|
||||
updated_at_micros: 100,
|
||||
published_at_micros: Some(100),
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct BigFishWorkSummaryResponse {
|
||||
pub cover_image_src: Option<String>,
|
||||
pub status: String,
|
||||
pub updated_at: String,
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub level_count: u32,
|
||||
pub level_main_image_ready_count: u32,
|
||||
@@ -20,6 +22,10 @@ pub struct BigFishWorkSummaryResponse {
|
||||
pub background_ready: bool,
|
||||
#[serde(default)]
|
||||
pub play_count: u32,
|
||||
#[serde(default)]
|
||||
pub remix_count: u32,
|
||||
#[serde(default)]
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -35,6 +35,10 @@ pub struct PuzzleWorkSummaryResponse {
|
||||
#[serde(default)]
|
||||
pub published_at: Option<String>,
|
||||
pub play_count: u32,
|
||||
#[serde(default)]
|
||||
pub remix_count: u32,
|
||||
#[serde(default)]
|
||||
pub like_count: u32,
|
||||
pub publish_ready: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -437,6 +437,12 @@ pub struct CustomWorldLibraryEntryResponse {
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
#[serde(default)]
|
||||
pub play_count: u32,
|
||||
#[serde(default)]
|
||||
pub remix_count: u32,
|
||||
#[serde(default)]
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -457,6 +463,12 @@ pub struct CustomWorldGalleryCardResponse {
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
#[serde(default)]
|
||||
pub play_count: u32,
|
||||
#[serde(default)]
|
||||
pub remix_count: u32,
|
||||
#[serde(default)]
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::mapper::*;
|
||||
use crate::module_bindings::delete_big_fish_work_procedure::delete_big_fish_work;
|
||||
use crate::module_bindings::record_big_fish_play_procedure::record_big_fish_play;
|
||||
use crate::module_bindings::remix_big_fish_work_procedure::remix_big_fish_work;
|
||||
|
||||
impl SpacetimeClient {
|
||||
pub async fn create_big_fish_session(
|
||||
@@ -290,4 +291,29 @@ impl SpacetimeClient {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remix_big_fish_work(
|
||||
&self,
|
||||
input: BigFishWorkRemixRecordInput,
|
||||
) -> Result<BigFishSessionRecord, SpacetimeClientError> {
|
||||
let procedure_input = BigFishWorkRemixInput {
|
||||
source_session_id: input.source_session_id,
|
||||
target_session_id: input.target_session_id,
|
||||
target_owner_user_id: input.target_owner_user_id,
|
||||
welcome_message_id: input.welcome_message_id,
|
||||
remixed_at_micros: input.remixed_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.remix_big_fish_work_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_session_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::*;
|
||||
use crate::mapper::*;
|
||||
use crate::module_bindings::delete_custom_world_agent_session_procedure::delete_custom_world_agent_session;
|
||||
use crate::module_bindings::record_custom_world_profile_play_procedure::record_custom_world_profile_play;
|
||||
use crate::module_bindings::remix_custom_world_profile_procedure::remix_custom_world_profile;
|
||||
|
||||
impl SpacetimeClient {
|
||||
pub async fn list_custom_world_profiles(
|
||||
@@ -209,6 +211,55 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remix_custom_world_profile(
|
||||
&self,
|
||||
input: CustomWorldProfileRemixRecordInput,
|
||||
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||
let procedure_input = CustomWorldProfileRemixInput {
|
||||
source_owner_user_id: input.source_owner_user_id,
|
||||
source_profile_id: input.source_profile_id,
|
||||
target_owner_user_id: input.target_owner_user_id,
|
||||
target_profile_id: input.target_profile_id,
|
||||
author_display_name: input.author_display_name,
|
||||
remixed_at_micros: input.remixed_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.remix_custom_world_profile_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_custom_world_library_mutation_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn record_custom_world_profile_play(
|
||||
&self,
|
||||
input: CustomWorldProfilePlayReportRecordInput,
|
||||
) -> Result<CustomWorldLibraryMutationRecord, SpacetimeClientError> {
|
||||
let procedure_input = CustomWorldProfilePlayRecordInput {
|
||||
owner_user_id: input.owner_user_id,
|
||||
profile_id: input.profile_id,
|
||||
played_at_micros: input.played_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.record_custom_world_profile_play_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_custom_world_library_mutation_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn publish_custom_world_world(
|
||||
&self,
|
||||
input: CustomWorldPublishWorldRecordInput,
|
||||
|
||||
@@ -20,8 +20,9 @@ pub use mapper::{
|
||||
CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord,
|
||||
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
|
||||
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord,
|
||||
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
|
||||
CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput,
|
||||
CustomWorldProfilePlayReportRecordInput, CustomWorldProfileRemixRecordInput,
|
||||
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord, CustomWorldPublishWorldRecord,
|
||||
CustomWorldPublishWorldRecordInput,
|
||||
CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord,
|
||||
CustomWorldSupportedActionRecord, CustomWorldWorkSummaryRecord, NpcBattleInteractionRecord,
|
||||
NpcInteractionRecord, NpcStateRecord, PuzzleAgentMessageFinalizeRecordInput,
|
||||
@@ -33,10 +34,10 @@ pub use mapper::{
|
||||
PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord,
|
||||
PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
|
||||
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
|
||||
PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, PuzzleRunRecord,
|
||||
PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, PuzzleRunRecord, PuzzleWorkRemixRecordInput,
|
||||
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
|
||||
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
|
||||
ResolveCombatActionRecord, ResolveNpcBattleInteractionInput,
|
||||
ResolveCombatActionRecord, ResolveNpcBattleInteractionInput, BigFishWorkRemixRecordInput,
|
||||
};
|
||||
|
||||
pub mod ai;
|
||||
|
||||
@@ -1885,6 +1885,9 @@ pub(crate) fn map_custom_world_library_entry_from_profile_snapshot(
|
||||
.to_string(),
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1910,6 +1913,9 @@ pub(crate) fn map_custom_world_gallery_entry_snapshot(
|
||||
.to_string(),
|
||||
playable_npc_count: snapshot.playable_npc_count,
|
||||
landmark_count: snapshot.landmark_count,
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2387,6 +2393,8 @@ pub(crate) fn map_puzzle_work_profile(
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
published_at: snapshot.published_at_micros.map(format_timestamp_micros),
|
||||
play_count: snapshot.play_count,
|
||||
remix_count: snapshot.remix_count,
|
||||
like_count: snapshot.like_count,
|
||||
publish_ready: snapshot.publish_ready,
|
||||
anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack),
|
||||
}
|
||||
@@ -3937,6 +3945,9 @@ pub struct CustomWorldLibraryEntryRecord {
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -3956,6 +3967,9 @@ pub struct CustomWorldGalleryEntryRecord {
|
||||
pub theme_mode: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -4172,6 +4186,23 @@ pub struct CustomWorldProfileUpsertRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfileRemixRecordInput {
|
||||
pub source_owner_user_id: String,
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldProfilePlayReportRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldPublishWorldRecordInput {
|
||||
pub session_id: String,
|
||||
@@ -4334,6 +4365,18 @@ pub struct PuzzleWorkUpsertRecordInput {
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuzzleWorkRemixRecordInput {
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub target_work_id: String,
|
||||
pub author_display_name: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuzzleRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
@@ -4376,6 +4419,15 @@ pub struct BigFishPlayReportRecordInput {
|
||||
pub reported_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishWorkRemixRecordInput {
|
||||
pub source_session_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PuzzleAnchorItemRecord {
|
||||
pub key: String,
|
||||
@@ -4502,6 +4554,8 @@ pub struct PuzzleWorkProfileRecord {
|
||||
pub updated_at: String,
|
||||
pub published_at: Option<String>,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub publish_ready: bool,
|
||||
pub anchor_pack: PuzzleAnchorPackRecord,
|
||||
}
|
||||
@@ -4771,12 +4825,15 @@ pub struct BigFishWorkSummaryRecord {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
@@ -4791,6 +4848,8 @@ struct CompatibleBigFishWorkSummaryRecord {
|
||||
cover_image_src: Option<String>,
|
||||
status: String,
|
||||
updated_at_micros: i64,
|
||||
#[serde(default)]
|
||||
published_at_micros: Option<i64>,
|
||||
publish_ready: bool,
|
||||
level_count: u32,
|
||||
level_main_image_ready_count: u32,
|
||||
@@ -4798,6 +4857,10 @@ struct CompatibleBigFishWorkSummaryRecord {
|
||||
background_ready: bool,
|
||||
#[serde(default)]
|
||||
play_count: u32,
|
||||
#[serde(default)]
|
||||
remix_count: u32,
|
||||
#[serde(default)]
|
||||
like_count: u32,
|
||||
}
|
||||
|
||||
impl CompatibleBigFishWorkSummaryRecord {
|
||||
@@ -4817,12 +4880,15 @@ impl CompatibleBigFishWorkSummaryRecord {
|
||||
cover_image_src: self.cover_image_src,
|
||||
status: self.status,
|
||||
updated_at_micros: self.updated_at_micros,
|
||||
published_at_micros: self.published_at_micros,
|
||||
publish_ready: self.publish_ready,
|
||||
level_count: self.level_count,
|
||||
level_main_image_ready_count: self.level_main_image_ready_count,
|
||||
level_motion_ready_count: self.level_motion_ready_count,
|
||||
background_ready: self.background_ready,
|
||||
play_count: self.play_count,
|
||||
remix_count: self.remix_count,
|
||||
like_count: self.like_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4831,6 +4897,73 @@ impl CompatibleBigFishWorkSummaryRecord {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn puzzle_works_mapper_backfills_missing_public_stat_fields() {
|
||||
let result = PuzzleWorksProcedureResult {
|
||||
ok: true,
|
||||
items_json: Some(
|
||||
r#"[{
|
||||
"work_id":"puzzle-work-1",
|
||||
"profile_id":"puzzle-profile-1",
|
||||
"owner_user_id":"user-1",
|
||||
"source_session_id":null,
|
||||
"author_display_name":"测试作者",
|
||||
"level_name":"雨夜拼图",
|
||||
"summary":"旧公开作品摘要",
|
||||
"theme_tags":["雨夜","猫咪","神庙"],
|
||||
"cover_image_src":null,
|
||||
"cover_asset_id":null,
|
||||
"publication_status":"Published",
|
||||
"updated_at_micros":123000000,
|
||||
"published_at_micros":123000000,
|
||||
"publish_ready":true,
|
||||
"anchor_pack":{
|
||||
"theme_promise":{
|
||||
"key":"themePromise",
|
||||
"label":"题材承诺",
|
||||
"value":"雨夜冒险",
|
||||
"status":"Inferred"
|
||||
},
|
||||
"visual_subject":{
|
||||
"key":"visualSubject",
|
||||
"label":"画面主体",
|
||||
"value":"猫咪神庙",
|
||||
"status":"Inferred"
|
||||
},
|
||||
"visual_mood":{
|
||||
"key":"visualMood",
|
||||
"label":"视觉气质",
|
||||
"value":"温暖",
|
||||
"status":"Inferred"
|
||||
},
|
||||
"composition_hooks":{
|
||||
"key":"compositionHooks",
|
||||
"label":"拼图记忆点",
|
||||
"value":"灯光",
|
||||
"status":"Inferred"
|
||||
},
|
||||
"tags_and_forbidden":{
|
||||
"key":"tagsAndForbidden",
|
||||
"label":"标签与禁忌",
|
||||
"value":"雨夜, 猫咪, 神庙",
|
||||
"status":"Inferred"
|
||||
}
|
||||
}
|
||||
}]"#
|
||||
.to_string(),
|
||||
),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let items = map_puzzle_works_procedure_result(result)
|
||||
.expect("旧 puzzle works JSON 缺统计字段时应按 0 兼容");
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].play_count, 0);
|
||||
assert_eq!(items[0].remix_count, 0);
|
||||
assert_eq!(items[0].like_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn big_fish_works_mapper_backfills_missing_owner_user_id_for_private_lists() {
|
||||
let result = BigFishWorksProcedureResult {
|
||||
@@ -4861,6 +4994,10 @@ mod tests {
|
||||
|
||||
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, 0);
|
||||
assert_eq!(items[0].remix_count, 0);
|
||||
assert_eq!(items[0].like_count, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4893,6 +5030,10 @@ mod tests {
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert!(items[0].owner_user_id.is_empty());
|
||||
assert_eq!(items[0].published_at_micros, None);
|
||||
assert_eq!(items[0].play_count, 0);
|
||||
assert_eq!(items[0].remix_count, 0);
|
||||
assert_eq!(items[0].like_count, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ pub struct BigFishCreationSession {
|
||||
pub last_assistant_reply: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub published_at: Option<__sdk::Timestamp>,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
@@ -45,6 +48,9 @@ pub struct BigFishCreationSessionCols {
|
||||
pub last_assistant_reply: __sdk::__query_builder::Col<BigFishCreationSession, Option<String>>,
|
||||
pub publish_ready: __sdk::__query_builder::Col<BigFishCreationSession, bool>,
|
||||
pub play_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||
pub remix_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||
pub like_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||
pub published_at: __sdk::__query_builder::Col<BigFishCreationSession, Option<__sdk::Timestamp>>,
|
||||
pub created_at: __sdk::__query_builder::Col<BigFishCreationSession, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<BigFishCreationSession, __sdk::Timestamp>,
|
||||
}
|
||||
@@ -71,6 +77,9 @@ impl __sdk::__query_builder::HasCols for BigFishCreationSession {
|
||||
),
|
||||
publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct BigFishWorkRemixInput {
|
||||
pub source_session_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for BigFishWorkRemixInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -21,6 +21,9 @@ pub struct CustomWorldGalleryEntrySnapshot {
|
||||
pub theme_mode: CustomWorldThemeMode,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub published_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ pub struct CustomWorldGalleryEntry {
|
||||
pub theme_mode: CustomWorldThemeMode,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub published_at: __sdk::Timestamp,
|
||||
pub updated_at: __sdk::Timestamp,
|
||||
}
|
||||
@@ -45,6 +48,9 @@ pub struct CustomWorldGalleryEntryCols {
|
||||
pub theme_mode: __sdk::__query_builder::Col<CustomWorldGalleryEntry, CustomWorldThemeMode>,
|
||||
pub playable_npc_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||
pub landmark_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||
pub play_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||
pub remix_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||
pub like_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||
pub published_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
||||
pub updated_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
||||
}
|
||||
@@ -71,6 +77,9 @@ impl __sdk::__query_builder::HasCols for CustomWorldGalleryEntry {
|
||||
theme_mode: __sdk::__query_builder::Col::new(table_name, "theme_mode"),
|
||||
playable_npc_count: __sdk::__query_builder::Col::new(table_name, "playable_npc_count"),
|
||||
landmark_count: __sdk::__query_builder::Col::new(table_name, "landmark_count"),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct CustomWorldProfilePlayRecordInput {
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub played_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for CustomWorldProfilePlayRecordInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct CustomWorldProfileRemixInput {
|
||||
pub source_owner_user_id: String,
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub author_display_name: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for CustomWorldProfileRemixInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -24,6 +24,9 @@ pub struct CustomWorldProfileSnapshot {
|
||||
pub profile_payload_json: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub author_display_name: String,
|
||||
pub published_at_micros: Option<i64>,
|
||||
pub deleted_at_micros: Option<i64>,
|
||||
|
||||
@@ -24,6 +24,9 @@ pub struct CustomWorldProfile {
|
||||
pub profile_payload_json: String,
|
||||
pub playable_npc_count: u32,
|
||||
pub landmark_count: u32,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub author_display_name: String,
|
||||
pub published_at: Option<__sdk::Timestamp>,
|
||||
pub deleted_at: Option<__sdk::Timestamp>,
|
||||
@@ -54,6 +57,9 @@ pub struct CustomWorldProfileCols {
|
||||
pub profile_payload_json: __sdk::__query_builder::Col<CustomWorldProfile, String>,
|
||||
pub playable_npc_count: __sdk::__query_builder::Col<CustomWorldProfile, u32>,
|
||||
pub landmark_count: __sdk::__query_builder::Col<CustomWorldProfile, u32>,
|
||||
pub play_count: __sdk::__query_builder::Col<CustomWorldProfile, u32>,
|
||||
pub remix_count: __sdk::__query_builder::Col<CustomWorldProfile, u32>,
|
||||
pub like_count: __sdk::__query_builder::Col<CustomWorldProfile, u32>,
|
||||
pub author_display_name: __sdk::__query_builder::Col<CustomWorldProfile, String>,
|
||||
pub published_at: __sdk::__query_builder::Col<CustomWorldProfile, Option<__sdk::Timestamp>>,
|
||||
pub deleted_at: __sdk::__query_builder::Col<CustomWorldProfile, Option<__sdk::Timestamp>>,
|
||||
@@ -88,6 +94,9 @@ impl __sdk::__query_builder::HasCols for CustomWorldProfile {
|
||||
),
|
||||
playable_npc_count: __sdk::__query_builder::Col::new(table_name, "playable_npc_count"),
|
||||
landmark_count: __sdk::__query_builder::Col::new(table_name, "landmark_count"),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||
author_display_name: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"author_display_name",
|
||||
|
||||
@@ -99,6 +99,7 @@ pub mod big_fish_session_get_input_type;
|
||||
pub mod big_fish_session_procedure_result_type;
|
||||
pub mod big_fish_session_snapshot_type;
|
||||
pub mod big_fish_work_delete_input_type;
|
||||
pub mod big_fish_work_remix_input_type;
|
||||
pub mod big_fish_works_list_input_type;
|
||||
pub mod big_fish_works_procedure_result_type;
|
||||
pub mod bind_asset_object_to_entity_and_return_procedure;
|
||||
@@ -166,7 +167,9 @@ pub mod custom_world_library_mutation_result_type;
|
||||
pub mod custom_world_profile_delete_input_type;
|
||||
pub mod custom_world_profile_list_input_type;
|
||||
pub mod custom_world_profile_list_result_type;
|
||||
pub mod custom_world_profile_play_record_input_type;
|
||||
pub mod custom_world_profile_publish_input_type;
|
||||
pub mod custom_world_profile_remix_input_type;
|
||||
pub mod custom_world_profile_snapshot_type;
|
||||
pub mod custom_world_profile_type;
|
||||
pub mod custom_world_profile_unpublish_input_type;
|
||||
@@ -318,6 +321,7 @@ pub mod puzzle_work_delete_input_type;
|
||||
pub mod puzzle_work_get_input_type;
|
||||
pub mod puzzle_work_procedure_result_type;
|
||||
pub mod puzzle_work_profile_row_type;
|
||||
pub mod puzzle_work_remix_input_type;
|
||||
pub mod puzzle_work_upsert_input_type;
|
||||
pub mod puzzle_works_list_input_type;
|
||||
pub mod puzzle_works_procedure_result_type;
|
||||
@@ -349,10 +353,14 @@ pub mod quest_step_snapshot_type;
|
||||
pub mod quest_treasure_inspected_signal_type;
|
||||
pub mod quest_turn_in_input_type;
|
||||
pub mod record_big_fish_play_procedure;
|
||||
pub mod record_custom_world_profile_play_procedure;
|
||||
pub mod redeem_profile_referral_invite_code_procedure;
|
||||
pub mod redeem_profile_reward_code_procedure;
|
||||
pub mod refresh_session_type;
|
||||
pub mod refund_profile_wallet_points_and_return_procedure;
|
||||
pub mod remix_big_fish_work_procedure;
|
||||
pub mod remix_custom_world_profile_procedure;
|
||||
pub mod remix_puzzle_work_procedure;
|
||||
pub mod resolve_combat_action_and_return_procedure;
|
||||
pub mod resolve_combat_action_input_type;
|
||||
pub mod resolve_combat_action_procedure_result_type;
|
||||
@@ -583,6 +591,7 @@ pub use big_fish_session_get_input_type::BigFishSessionGetInput;
|
||||
pub use big_fish_session_procedure_result_type::BigFishSessionProcedureResult;
|
||||
pub use big_fish_session_snapshot_type::BigFishSessionSnapshot;
|
||||
pub use big_fish_work_delete_input_type::BigFishWorkDeleteInput;
|
||||
pub use big_fish_work_remix_input_type::BigFishWorkRemixInput;
|
||||
pub use big_fish_works_list_input_type::BigFishWorksListInput;
|
||||
pub use big_fish_works_procedure_result_type::BigFishWorksProcedureResult;
|
||||
pub use bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return;
|
||||
@@ -650,7 +659,9 @@ pub use custom_world_library_mutation_result_type::CustomWorldLibraryMutationRes
|
||||
pub use custom_world_profile_delete_input_type::CustomWorldProfileDeleteInput;
|
||||
pub use custom_world_profile_list_input_type::CustomWorldProfileListInput;
|
||||
pub use custom_world_profile_list_result_type::CustomWorldProfileListResult;
|
||||
pub use custom_world_profile_play_record_input_type::CustomWorldProfilePlayRecordInput;
|
||||
pub use custom_world_profile_publish_input_type::CustomWorldProfilePublishInput;
|
||||
pub use custom_world_profile_remix_input_type::CustomWorldProfileRemixInput;
|
||||
pub use custom_world_profile_snapshot_type::CustomWorldProfileSnapshot;
|
||||
pub use custom_world_profile_type::CustomWorldProfile;
|
||||
pub use custom_world_profile_unpublish_input_type::CustomWorldProfileUnpublishInput;
|
||||
@@ -802,6 +813,7 @@ pub use puzzle_work_delete_input_type::PuzzleWorkDeleteInput;
|
||||
pub use puzzle_work_get_input_type::PuzzleWorkGetInput;
|
||||
pub use puzzle_work_procedure_result_type::PuzzleWorkProcedureResult;
|
||||
pub use puzzle_work_profile_row_type::PuzzleWorkProfileRow;
|
||||
pub use puzzle_work_remix_input_type::PuzzleWorkRemixInput;
|
||||
pub use puzzle_work_upsert_input_type::PuzzleWorkUpsertInput;
|
||||
pub use puzzle_works_list_input_type::PuzzleWorksListInput;
|
||||
pub use puzzle_works_procedure_result_type::PuzzleWorksProcedureResult;
|
||||
@@ -833,10 +845,14 @@ pub use quest_step_snapshot_type::QuestStepSnapshot;
|
||||
pub use quest_treasure_inspected_signal_type::QuestTreasureInspectedSignal;
|
||||
pub use quest_turn_in_input_type::QuestTurnInInput;
|
||||
pub use record_big_fish_play_procedure::record_big_fish_play;
|
||||
pub use record_custom_world_profile_play_procedure::record_custom_world_profile_play;
|
||||
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
|
||||
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
|
||||
pub use refresh_session_type::RefreshSession;
|
||||
pub use refund_profile_wallet_points_and_return_procedure::refund_profile_wallet_points_and_return;
|
||||
pub use remix_big_fish_work_procedure::remix_big_fish_work;
|
||||
pub use remix_custom_world_profile_procedure::remix_custom_world_profile;
|
||||
pub use remix_puzzle_work_procedure::remix_puzzle_work;
|
||||
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
||||
pub use resolve_combat_action_input_type::ResolveCombatActionInput;
|
||||
pub use resolve_combat_action_procedure_result_type::ResolveCombatActionProcedureResult;
|
||||
|
||||
@@ -21,6 +21,8 @@ pub struct PuzzleWorkProfileRow {
|
||||
pub cover_asset_id: Option<String>,
|
||||
pub publication_status: PuzzlePublicationStatus,
|
||||
pub play_count: u32,
|
||||
pub remix_count: u32,
|
||||
pub like_count: u32,
|
||||
pub anchor_pack_json: String,
|
||||
pub publish_ready: bool,
|
||||
pub created_at: __sdk::Timestamp,
|
||||
@@ -49,6 +51,8 @@ pub struct PuzzleWorkProfileRowCols {
|
||||
pub publication_status:
|
||||
__sdk::__query_builder::Col<PuzzleWorkProfileRow, PuzzlePublicationStatus>,
|
||||
pub play_count: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u32>,
|
||||
pub remix_count: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u32>,
|
||||
pub like_count: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u32>,
|
||||
pub anchor_pack_json: __sdk::__query_builder::Col<PuzzleWorkProfileRow, String>,
|
||||
pub publish_ready: __sdk::__query_builder::Col<PuzzleWorkProfileRow, bool>,
|
||||
pub created_at: __sdk::__query_builder::Col<PuzzleWorkProfileRow, __sdk::Timestamp>,
|
||||
@@ -75,6 +79,8 @@ impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow {
|
||||
cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"),
|
||||
publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||
anchor_pack_json: __sdk::__query_builder::Col::new(table_name, "anchor_pack_json"),
|
||||
publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"),
|
||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct PuzzleWorkRemixInput {
|
||||
pub source_profile_id: String,
|
||||
pub target_owner_user_id: String,
|
||||
pub target_session_id: String,
|
||||
pub target_profile_id: String,
|
||||
pub target_work_id: String,
|
||||
pub author_display_name: String,
|
||||
pub welcome_message_id: String,
|
||||
pub remixed_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for PuzzleWorkRemixInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::custom_world_library_mutation_result_type::CustomWorldLibraryMutationResult;
|
||||
use super::custom_world_profile_play_record_input_type::CustomWorldProfilePlayRecordInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RecordCustomWorldProfilePlayArgs {
|
||||
pub input: CustomWorldProfilePlayRecordInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RecordCustomWorldProfilePlayArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `record_custom_world_profile_play`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait record_custom_world_profile_play {
|
||||
fn record_custom_world_profile_play(&self, input: CustomWorldProfilePlayRecordInput) {
|
||||
self.record_custom_world_profile_play_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn record_custom_world_profile_play_then(
|
||||
&self,
|
||||
input: CustomWorldProfilePlayRecordInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CustomWorldLibraryMutationResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl record_custom_world_profile_play for super::RemoteProcedures {
|
||||
fn record_custom_world_profile_play_then(
|
||||
&self,
|
||||
input: CustomWorldProfilePlayRecordInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CustomWorldLibraryMutationResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, CustomWorldLibraryMutationResult>(
|
||||
"record_custom_world_profile_play",
|
||||
RecordCustomWorldProfilePlayArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::big_fish_session_procedure_result_type::BigFishSessionProcedureResult;
|
||||
use super::big_fish_work_remix_input_type::BigFishWorkRemixInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RemixBigFishWorkArgs {
|
||||
pub input: BigFishWorkRemixInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RemixBigFishWorkArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `remix_big_fish_work`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait remix_big_fish_work {
|
||||
fn remix_big_fish_work(&self, input: BigFishWorkRemixInput) {
|
||||
self.remix_big_fish_work_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn remix_big_fish_work_then(
|
||||
&self,
|
||||
input: BigFishWorkRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl remix_big_fish_work for super::RemoteProcedures {
|
||||
fn remix_big_fish_work_then(
|
||||
&self,
|
||||
input: BigFishWorkRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<BigFishSessionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, BigFishSessionProcedureResult>(
|
||||
"remix_big_fish_work",
|
||||
RemixBigFishWorkArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::custom_world_library_mutation_result_type::CustomWorldLibraryMutationResult;
|
||||
use super::custom_world_profile_remix_input_type::CustomWorldProfileRemixInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RemixCustomWorldProfileArgs {
|
||||
pub input: CustomWorldProfileRemixInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RemixCustomWorldProfileArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `remix_custom_world_profile`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait remix_custom_world_profile {
|
||||
fn remix_custom_world_profile(&self, input: CustomWorldProfileRemixInput) {
|
||||
self.remix_custom_world_profile_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn remix_custom_world_profile_then(
|
||||
&self,
|
||||
input: CustomWorldProfileRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CustomWorldLibraryMutationResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl remix_custom_world_profile for super::RemoteProcedures {
|
||||
fn remix_custom_world_profile_then(
|
||||
&self,
|
||||
input: CustomWorldProfileRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<CustomWorldLibraryMutationResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, CustomWorldLibraryMutationResult>(
|
||||
"remix_custom_world_profile",
|
||||
RemixCustomWorldProfileArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult;
|
||||
use super::puzzle_work_remix_input_type::PuzzleWorkRemixInput;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct RemixPuzzleWorkArgs {
|
||||
pub input: PuzzleWorkRemixInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RemixPuzzleWorkArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `remix_puzzle_work`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait remix_puzzle_work {
|
||||
fn remix_puzzle_work(&self, input: PuzzleWorkRemixInput) {
|
||||
self.remix_puzzle_work_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn remix_puzzle_work_then(
|
||||
&self,
|
||||
input: PuzzleWorkRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl remix_puzzle_work for super::RemoteProcedures {
|
||||
fn remix_puzzle_work_then(
|
||||
&self,
|
||||
input: PuzzleWorkRemixInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<PuzzleAgentSessionProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, PuzzleAgentSessionProcedureResult>(
|
||||
"remix_puzzle_work",
|
||||
RemixPuzzleWorkArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::mapper::*;
|
||||
use crate::module_bindings::delete_puzzle_work_procedure::delete_puzzle_work;
|
||||
use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work;
|
||||
|
||||
impl SpacetimeClient {
|
||||
pub async fn create_puzzle_agent_session(
|
||||
@@ -340,6 +341,34 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remix_puzzle_work(
|
||||
&self,
|
||||
input: PuzzleWorkRemixRecordInput,
|
||||
) -> Result<PuzzleAgentSessionRecord, SpacetimeClientError> {
|
||||
let procedure_input = PuzzleWorkRemixInput {
|
||||
source_profile_id: input.source_profile_id,
|
||||
target_owner_user_id: input.target_owner_user_id,
|
||||
target_session_id: input.target_session_id,
|
||||
target_profile_id: input.target_profile_id,
|
||||
target_work_id: input.target_work_id,
|
||||
author_display_name: input.author_display_name,
|
||||
welcome_message_id: input.welcome_message_id,
|
||||
remixed_at_micros: input.remixed_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.remix_puzzle_work_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_puzzle_agent_session_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_puzzle_run(
|
||||
&self,
|
||||
input: PuzzleRunStartRecordInput,
|
||||
|
||||
@@ -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