Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
320 lines
10 KiB
Rust
320 lines
10 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::creation_audio::CreationAudioAsset;
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PutMatch3DWorkRequest {
|
|
pub game_name: String,
|
|
#[serde(default)]
|
|
pub theme_text: Option<String>,
|
|
pub summary: String,
|
|
pub tags: Vec<String>,
|
|
#[serde(default)]
|
|
pub cover_image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub reference_image_src: Option<String>,
|
|
pub clear_count: u32,
|
|
pub difficulty: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DWorkTagsRequest {
|
|
pub game_name: String,
|
|
pub theme_text: String,
|
|
#[serde(default)]
|
|
pub summary: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DWorkTagsResponse {
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PutMatch3DAudioAssetsRequest {
|
|
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PersistMatch3DGeneratedModelRequest {
|
|
pub item_id: String,
|
|
pub item_name: String,
|
|
pub source_url: String,
|
|
#[serde(default)]
|
|
pub file_name: Option<String>,
|
|
#[serde(default)]
|
|
pub task_uuid: Option<String>,
|
|
#[serde(default)]
|
|
pub subscription_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PersistMatch3DGeneratedModelResponse {
|
|
pub asset: Match3DGeneratedItemAssetResponse,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DCoverImageRequest {
|
|
pub prompt: String,
|
|
#[serde(default)]
|
|
pub uploaded_image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub reference_image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub reference_image_srcs: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DCoverImageResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
pub cover_image_src: String,
|
|
pub cover_image_object_key: String,
|
|
pub prompt: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DBackgroundImageRequest {
|
|
pub prompt: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DBackgroundImageResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
pub background_image_src: String,
|
|
pub background_image_object_key: String,
|
|
pub generated_background_asset: Match3DGeneratedBackgroundAssetResponse,
|
|
pub prompt: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DContainerImageRequest {
|
|
pub prompt: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DContainerImageResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
pub container_image_src: String,
|
|
pub container_image_object_key: String,
|
|
pub generated_background_asset: Match3DGeneratedBackgroundAssetResponse,
|
|
pub prompt: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DItemAssetsRequest {
|
|
pub item_names: Vec<String>,
|
|
#[serde(default)]
|
|
pub mode: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct GenerateMatch3DItemAssetsResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DWorkSummaryResponse {
|
|
pub work_id: String,
|
|
pub profile_id: String,
|
|
pub owner_user_id: String,
|
|
#[serde(default)]
|
|
pub source_session_id: Option<String>,
|
|
pub game_name: String,
|
|
pub theme_text: String,
|
|
pub summary: String,
|
|
pub tags: Vec<String>,
|
|
#[serde(default)]
|
|
pub cover_image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub reference_image_src: Option<String>,
|
|
pub clear_count: u32,
|
|
pub difficulty: u32,
|
|
pub publication_status: String,
|
|
pub play_count: u32,
|
|
pub updated_at: String,
|
|
#[serde(default)]
|
|
pub published_at: Option<String>,
|
|
pub publish_ready: bool,
|
|
#[serde(default)]
|
|
pub generation_status: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub background_prompt: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub background_image_src: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub background_image_object_key: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub generated_background_asset: Option<Match3DGeneratedBackgroundAssetResponse>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub generated_item_assets: Vec<Match3DGeneratedItemAssetResponse>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DGeneratedBackgroundAssetResponse {
|
|
pub prompt: String,
|
|
#[serde(default)]
|
|
pub image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub image_object_key: Option<String>,
|
|
#[serde(default)]
|
|
pub container_prompt: Option<String>,
|
|
#[serde(default)]
|
|
pub container_image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub container_image_object_key: Option<String>,
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DGeneratedItemImageViewResponse {
|
|
pub view_id: String,
|
|
pub view_index: u32,
|
|
#[serde(default)]
|
|
pub image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub image_object_key: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DGeneratedItemAssetResponse {
|
|
pub item_id: String,
|
|
pub item_name: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub item_size: Option<String>,
|
|
#[serde(default)]
|
|
pub image_src: Option<String>,
|
|
#[serde(default)]
|
|
pub image_object_key: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
pub image_views: Vec<Match3DGeneratedItemImageViewResponse>,
|
|
#[serde(default)]
|
|
pub model_src: Option<String>,
|
|
#[serde(default)]
|
|
pub model_object_key: Option<String>,
|
|
#[serde(default)]
|
|
pub model_file_name: Option<String>,
|
|
#[serde(default)]
|
|
pub task_uuid: Option<String>,
|
|
#[serde(default)]
|
|
pub subscription_key: Option<String>,
|
|
#[serde(default)]
|
|
pub sound_prompt: Option<String>,
|
|
#[serde(default)]
|
|
pub background_music_title: Option<String>,
|
|
#[serde(default)]
|
|
pub background_music_style: Option<String>,
|
|
#[serde(default)]
|
|
pub background_music_prompt: Option<String>,
|
|
#[serde(default)]
|
|
pub background_music: Option<CreationAudioAsset>,
|
|
#[serde(default)]
|
|
pub click_sound: Option<CreationAudioAsset>,
|
|
#[serde(default)]
|
|
pub background_asset: Option<Match3DGeneratedBackgroundAssetResponse>,
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DWorkProfileResponse {
|
|
#[serde(flatten)]
|
|
pub summary: Match3DWorkSummaryResponse,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DWorksResponse {
|
|
pub items: Vec<Match3DWorkSummaryResponse>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DWorkDetailResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Match3DWorkMutationResponse {
|
|
pub item: Match3DWorkProfileResponse,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn match3d_work_request_uses_camel_case() {
|
|
let payload = serde_json::to_value(PutMatch3DWorkRequest {
|
|
game_name: "水果抓大鹅".to_string(),
|
|
theme_text: Some("水果".to_string()),
|
|
summary: "水果主题".to_string(),
|
|
tags: vec!["水果".to_string()],
|
|
cover_image_src: None,
|
|
reference_image_src: None,
|
|
clear_count: 4,
|
|
difficulty: 5,
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload["gameName"], json!("水果抓大鹅"));
|
|
assert_eq!(payload["clearCount"], json!(4));
|
|
}
|
|
|
|
#[test]
|
|
fn match3d_work_summary_uses_camel_case_generation_status() {
|
|
let payload = serde_json::to_value(Match3DWorkSummaryResponse {
|
|
work_id: "work-1".to_string(),
|
|
profile_id: "profile-1".to_string(),
|
|
owner_user_id: "user-1".to_string(),
|
|
source_session_id: Some("session-1".to_string()),
|
|
game_name: "水果抓大鹅".to_string(),
|
|
theme_text: "水果".to_string(),
|
|
summary: "水果主题".to_string(),
|
|
tags: vec!["水果".to_string()],
|
|
cover_image_src: None,
|
|
reference_image_src: None,
|
|
clear_count: 4,
|
|
difficulty: 5,
|
|
publication_status: "draft".to_string(),
|
|
play_count: 0,
|
|
updated_at: "2026-05-01T00:00:00Z".to_string(),
|
|
published_at: None,
|
|
publish_ready: false,
|
|
generation_status: Some("generating".to_string()),
|
|
background_prompt: None,
|
|
background_image_src: None,
|
|
background_image_object_key: None,
|
|
generated_background_asset: None,
|
|
generated_item_assets: Vec::new(),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload["generationStatus"], json!("generating"));
|
|
}
|
|
}
|