Extend square-hole creation flow with visual asset timeout guard
This commit is contained in:
@@ -10,12 +10,17 @@ use module_square_hole::{
|
||||
SquareHoleDropFeedback as DomainSquareHoleDropFeedback,
|
||||
SquareHoleDropInput as DomainSquareHoleDropInput,
|
||||
SquareHoleDropRejectReason as DomainSquareHoleDropRejectReason,
|
||||
SquareHoleHoleOption as DomainSquareHoleHoleOption,
|
||||
SquareHoleHoleSnapshot as DomainSquareHoleHoleSnapshot,
|
||||
SquareHoleRunSnapshot as DomainSquareHoleRunSnapshot,
|
||||
SquareHoleRunStatus as DomainSquareHoleRunStatus,
|
||||
SquareHoleShapeOption as DomainSquareHoleShapeOption,
|
||||
SquareHoleShapeSnapshot as DomainSquareHoleShapeSnapshot,
|
||||
build_creator_config as build_domain_creator_config,
|
||||
compile_result_draft as compile_domain_result_draft, confirm_drop_at as confirm_domain_drop_at,
|
||||
default_background_prompt as default_domain_background_prompt,
|
||||
normalize_hole_options as normalize_domain_hole_options,
|
||||
normalize_shape_options as normalize_domain_shape_options,
|
||||
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_at as start_domain_run_at,
|
||||
stop_run_at as stop_domain_run_at,
|
||||
};
|
||||
@@ -432,6 +437,9 @@ fn compile_square_hole_draft_tx(
|
||||
domain_draft.blockers = module_square_hole::validate_publish_requirements(&domain_draft);
|
||||
domain_draft.publish_ready = domain_draft.blockers.is_empty();
|
||||
|
||||
let cover_image_src = clean_optional(&input.cover_image_src)
|
||||
.or_else(|| clean_optional(&Some(config.cover_image_src.clone())))
|
||||
.unwrap_or_default();
|
||||
let draft = SquareHoleDraftSnapshot {
|
||||
profile_id: input.profile_id.clone(),
|
||||
game_name: domain_draft.game_name.clone(),
|
||||
@@ -439,6 +447,14 @@ fn compile_square_hole_draft_tx(
|
||||
twist_rule: domain_draft.twist_rule.clone(),
|
||||
summary_text: domain_draft.summary.clone(),
|
||||
tags: domain_draft.tags.clone(),
|
||||
cover_image_src: cover_image_src.clone(),
|
||||
background_prompt: domain_draft.background_prompt.clone(),
|
||||
background_image_src: domain_draft
|
||||
.background_image_src
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
shape_options: shape_options_to_snapshot(&domain_draft.shape_options),
|
||||
hole_options: hole_options_to_snapshot(&domain_draft.hole_options),
|
||||
shape_count: domain_draft.shape_count,
|
||||
difficulty: domain_draft.difficulty,
|
||||
};
|
||||
@@ -454,7 +470,7 @@ fn compile_square_hole_draft_tx(
|
||||
twist_rule: config.twist_rule.clone(),
|
||||
summary_text: draft.summary_text.clone(),
|
||||
tags_json: to_json_string(&draft.tags),
|
||||
cover_image_src: clean_optional(&input.cover_image_src).unwrap_or_default(),
|
||||
cover_image_src,
|
||||
shape_count: config.shape_count,
|
||||
difficulty: config.difficulty,
|
||||
config_json: to_json_string(&config),
|
||||
@@ -493,12 +509,31 @@ fn update_square_hole_work_tx(
|
||||
) -> Result<SquareHoleWorkSnapshot, String> {
|
||||
let current = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
|
||||
let tags = parse_tags(&input.tags_json)?;
|
||||
let config = SquareHoleCreatorConfigSnapshot {
|
||||
let current_config = parse_config(¤t.config_json)?;
|
||||
let shape_options = parse_optional_json::<Vec<SquareHoleShapeOptionSnapshot>>(
|
||||
input.shape_options_json.as_str(),
|
||||
"square_hole shape_options_json",
|
||||
)?
|
||||
.unwrap_or_else(|| current_config.shape_options.clone());
|
||||
let hole_options = parse_optional_json::<Vec<SquareHoleHoleOptionSnapshot>>(
|
||||
input.hole_options_json.as_str(),
|
||||
"square_hole hole_options_json",
|
||||
)?
|
||||
.unwrap_or_else(|| current_config.hole_options.clone());
|
||||
let config = normalize_config(SquareHoleCreatorConfigSnapshot {
|
||||
theme_text: clean_string(&input.theme_text, "玩具"),
|
||||
twist_rule: clean_string(&input.twist_rule, "方洞万能"),
|
||||
shape_count: input.shape_count,
|
||||
difficulty: input.difficulty,
|
||||
};
|
||||
shape_options,
|
||||
hole_options,
|
||||
background_prompt: clean_string(
|
||||
&input.background_prompt,
|
||||
&default_domain_background_prompt(&input.theme_text),
|
||||
),
|
||||
cover_image_src: input.cover_image_src.trim().to_string(),
|
||||
background_image_src: input.background_image_src.trim().to_string(),
|
||||
});
|
||||
validate_config(&config)?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
let next = SquareHoleWorkProfileRow {
|
||||
@@ -828,6 +863,10 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result<SquareHoleWorkS
|
||||
summary_text: row.summary_text.clone(),
|
||||
tags: parse_tags(&row.tags_json)?,
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
background_prompt: config.background_prompt.clone(),
|
||||
background_image_src: config.background_image_src.clone(),
|
||||
shape_options: config.shape_options.clone(),
|
||||
hole_options: config.hole_options.clone(),
|
||||
shape_count: row.shape_count,
|
||||
difficulty: row.difficulty,
|
||||
config,
|
||||
@@ -1040,12 +1079,17 @@ fn is_work_publish_ready(row: &SquareHoleWorkProfileRow) -> bool {
|
||||
}
|
||||
|
||||
fn default_config_from_seed(seed_text: &str) -> SquareHoleCreatorConfigSnapshot {
|
||||
SquareHoleCreatorConfigSnapshot {
|
||||
normalize_config(SquareHoleCreatorConfigSnapshot {
|
||||
theme_text: clean_string(seed_text, "玩具"),
|
||||
twist_rule: "方洞万能".to_string(),
|
||||
shape_count: 12,
|
||||
difficulty: 4,
|
||||
}
|
||||
shape_options: Vec::new(),
|
||||
hole_options: Vec::new(),
|
||||
background_prompt: String::new(),
|
||||
cover_image_src: String::new(),
|
||||
background_image_src: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
|
||||
@@ -1053,17 +1097,34 @@ fn parse_config_or_default(value: &str) -> SquareHoleCreatorConfigSnapshot {
|
||||
}
|
||||
|
||||
fn parse_config(value: &str) -> Result<SquareHoleCreatorConfigSnapshot, String> {
|
||||
parse_json(value, "square_hole config_json").map(
|
||||
|mut config: SquareHoleCreatorConfigSnapshot| {
|
||||
config.theme_text = clean_string(&config.theme_text, "玩具");
|
||||
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
|
||||
config.difficulty = config.difficulty.clamp(
|
||||
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
|
||||
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
|
||||
);
|
||||
config
|
||||
},
|
||||
)
|
||||
parse_json(value, "square_hole config_json").map(normalize_config)
|
||||
}
|
||||
|
||||
fn normalize_config(
|
||||
mut config: SquareHoleCreatorConfigSnapshot,
|
||||
) -> SquareHoleCreatorConfigSnapshot {
|
||||
config.theme_text = clean_string(&config.theme_text, "玩具");
|
||||
config.twist_rule = clean_string(&config.twist_rule, "方洞万能");
|
||||
config.difficulty = config.difficulty.clamp(
|
||||
module_square_hole::SQUARE_HOLE_MIN_DIFFICULTY,
|
||||
module_square_hole::SQUARE_HOLE_MAX_DIFFICULTY,
|
||||
);
|
||||
config.background_prompt = clean_string(
|
||||
&config.background_prompt,
|
||||
&default_domain_background_prompt(&config.theme_text),
|
||||
);
|
||||
config.cover_image_src = config.cover_image_src.trim().to_string();
|
||||
config.background_image_src = config.background_image_src.trim().to_string();
|
||||
|
||||
let shape_options = normalize_domain_shape_options(
|
||||
domain_shape_options_from_snapshot(&config.shape_options),
|
||||
&config.theme_text,
|
||||
);
|
||||
let hole_options =
|
||||
normalize_domain_hole_options(domain_hole_options_from_snapshot(&config.hole_options));
|
||||
config.shape_options = shape_options_to_snapshot(&shape_options);
|
||||
config.hole_options = hole_options_to_snapshot(&hole_options);
|
||||
config
|
||||
}
|
||||
|
||||
fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
||||
@@ -1071,6 +1132,13 @@ fn parse_tags(value: &str) -> Result<Vec<String>, String> {
|
||||
Ok(normalize_tags(parsed))
|
||||
}
|
||||
|
||||
fn parse_optional_json<T: DeserializeOwned>(value: &str, label: &str) -> Result<Option<T>, String> {
|
||||
if value.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
parse_json(value, label).map(Some)
|
||||
}
|
||||
|
||||
fn normalize_tags(tags: Vec<String>) -> Vec<String> {
|
||||
let mut result = Vec::new();
|
||||
for tag in tags {
|
||||
@@ -1097,12 +1165,18 @@ fn normalize_stage(value: &str) -> String {
|
||||
fn domain_config_from_snapshot(
|
||||
config: &SquareHoleCreatorConfigSnapshot,
|
||||
) -> Result<DomainSquareHoleCreatorConfig, module_square_hole::SquareHoleError> {
|
||||
build_domain_creator_config(
|
||||
let mut domain = build_domain_creator_config(
|
||||
&config.theme_text,
|
||||
&config.twist_rule,
|
||||
config.shape_count,
|
||||
config.difficulty,
|
||||
)
|
||||
)?;
|
||||
domain.shape_options = domain_shape_options_from_snapshot(&config.shape_options);
|
||||
domain.hole_options = domain_hole_options_from_snapshot(&config.hole_options);
|
||||
domain.background_prompt = config.background_prompt.clone();
|
||||
domain.cover_image_src = empty_to_none(&config.cover_image_src);
|
||||
domain.background_image_src = empty_to_none(&config.background_image_src);
|
||||
Ok(domain)
|
||||
}
|
||||
|
||||
fn snapshot_from_domain(
|
||||
@@ -1125,6 +1199,8 @@ fn snapshot_from_domain(
|
||||
best_combo: run.best_combo,
|
||||
score: run.score,
|
||||
rule_label: run.rule_label.clone(),
|
||||
background_image_src: run.background_image_src.clone().unwrap_or_default(),
|
||||
shape_options: shape_options_to_snapshot(&run.shape_options),
|
||||
current_shape: run.current_shape.as_ref().map(shape_from_domain),
|
||||
holes: run.holes.iter().map(hole_from_domain).collect(),
|
||||
last_feedback: run.last_feedback.as_ref().map(feedback_from_domain),
|
||||
@@ -1150,6 +1226,8 @@ fn domain_snapshot_from_snapshot(
|
||||
best_combo: snapshot.best_combo,
|
||||
score: snapshot.score,
|
||||
rule_label: snapshot.rule_label.clone(),
|
||||
background_image_src: empty_to_none(&snapshot.background_image_src),
|
||||
shape_options: domain_shape_options_from_snapshot(&snapshot.shape_options),
|
||||
current_shape: snapshot
|
||||
.current_shape
|
||||
.as_ref()
|
||||
@@ -1172,6 +1250,7 @@ fn shape_from_domain(shape: &DomainSquareHoleShapeSnapshot) -> SquareHoleShapeSn
|
||||
shape_kind: shape.shape_kind.clone(),
|
||||
label: shape.label.clone(),
|
||||
color: shape.color.clone(),
|
||||
image_src: shape.image_src.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,6 +1260,7 @@ fn domain_shape_from_snapshot(shape: &SquareHoleShapeSnapshot) -> DomainSquareHo
|
||||
shape_kind: shape.shape_kind.clone(),
|
||||
label: shape.label.clone(),
|
||||
color: shape.color.clone(),
|
||||
image_src: empty_to_none(&shape.image_src),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1191,6 +1271,7 @@ fn hole_from_domain(hole: &DomainSquareHoleHoleSnapshot) -> SquareHoleHoleSnapsh
|
||||
label: hole.label.clone(),
|
||||
x: hole.x,
|
||||
y: hole.y,
|
||||
bonus: hole.bonus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1201,9 +1282,68 @@ fn domain_hole_from_snapshot(hole: &SquareHoleHoleSnapshot) -> DomainSquareHoleH
|
||||
label: hole.label.clone(),
|
||||
x: hole.x,
|
||||
y: hole.y,
|
||||
bonus: hole.bonus,
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_options_to_snapshot(
|
||||
options: &[DomainSquareHoleShapeOption],
|
||||
) -> Vec<SquareHoleShapeOptionSnapshot> {
|
||||
options
|
||||
.iter()
|
||||
.map(|option| SquareHoleShapeOptionSnapshot {
|
||||
option_id: option.option_id.clone(),
|
||||
shape_kind: option.shape_kind.clone(),
|
||||
label: option.label.clone(),
|
||||
image_prompt: option.image_prompt.clone(),
|
||||
image_src: option.image_src.clone().unwrap_or_default(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn domain_shape_options_from_snapshot(
|
||||
options: &[SquareHoleShapeOptionSnapshot],
|
||||
) -> Vec<DomainSquareHoleShapeOption> {
|
||||
options
|
||||
.iter()
|
||||
.map(|option| DomainSquareHoleShapeOption {
|
||||
option_id: option.option_id.clone(),
|
||||
shape_kind: option.shape_kind.clone(),
|
||||
label: option.label.clone(),
|
||||
image_prompt: option.image_prompt.clone(),
|
||||
image_src: empty_to_none(&option.image_src),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hole_options_to_snapshot(
|
||||
options: &[DomainSquareHoleHoleOption],
|
||||
) -> Vec<SquareHoleHoleOptionSnapshot> {
|
||||
options
|
||||
.iter()
|
||||
.map(|option| SquareHoleHoleOptionSnapshot {
|
||||
hole_id: option.hole_id.clone(),
|
||||
hole_kind: option.hole_kind.clone(),
|
||||
label: option.label.clone(),
|
||||
bonus: option.bonus,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn domain_hole_options_from_snapshot(
|
||||
options: &[SquareHoleHoleOptionSnapshot],
|
||||
) -> Vec<DomainSquareHoleHoleOption> {
|
||||
options
|
||||
.iter()
|
||||
.map(|option| DomainSquareHoleHoleOption {
|
||||
hole_id: option.hole_id.clone(),
|
||||
hole_kind: option.hole_kind.clone(),
|
||||
label: option.label.clone(),
|
||||
bonus: option.bonus,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn feedback_from_domain(feedback: &DomainSquareHoleDropFeedback) -> SquareHoleDropFeedbackSnapshot {
|
||||
SquareHoleDropFeedbackSnapshot {
|
||||
accepted: feedback.accepted,
|
||||
|
||||
@@ -85,6 +85,10 @@ pub struct SquareHoleWorkUpdateInput {
|
||||
pub summary_text: String,
|
||||
pub tags_json: String,
|
||||
pub cover_image_src: String,
|
||||
pub background_prompt: String,
|
||||
pub background_image_src: String,
|
||||
pub shape_options_json: String,
|
||||
pub hole_options_json: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub updated_at_micros: i64,
|
||||
@@ -206,6 +210,37 @@ pub struct SquareHoleCreatorConfigSnapshot {
|
||||
pub twist_rule: String,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleShapeOptionSnapshot {
|
||||
pub option_id: String,
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub image_prompt: String,
|
||||
#[serde(default)]
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SquareHoleHoleOptionSnapshot {
|
||||
pub hole_id: String,
|
||||
pub hole_kind: String,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub bonus: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -228,6 +263,16 @@ pub struct SquareHoleDraftSnapshot {
|
||||
pub twist_rule: String,
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub cover_image_src: String,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: String,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
@@ -264,6 +309,14 @@ pub struct SquareHoleWorkSnapshot {
|
||||
pub summary_text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub cover_image_src: String,
|
||||
#[serde(default)]
|
||||
pub background_prompt: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: String,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
#[serde(default)]
|
||||
pub hole_options: Vec<SquareHoleHoleOptionSnapshot>,
|
||||
pub shape_count: u32,
|
||||
pub difficulty: u32,
|
||||
pub config: SquareHoleCreatorConfigSnapshot,
|
||||
@@ -281,6 +334,8 @@ pub struct SquareHoleShapeSnapshot {
|
||||
pub shape_kind: String,
|
||||
pub label: String,
|
||||
pub color: String,
|
||||
#[serde(default)]
|
||||
pub image_src: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -291,6 +346,8 @@ pub struct SquareHoleHoleSnapshot {
|
||||
pub label: String,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
#[serde(default)]
|
||||
pub bonus: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -319,6 +376,10 @@ pub struct SquareHoleRunSnapshot {
|
||||
pub best_combo: u32,
|
||||
pub score: u32,
|
||||
pub rule_label: String,
|
||||
#[serde(default)]
|
||||
pub background_image_src: String,
|
||||
#[serde(default)]
|
||||
pub shape_options: Vec<SquareHoleShapeOptionSnapshot>,
|
||||
pub current_shape: Option<SquareHoleShapeSnapshot>,
|
||||
pub holes: Vec<SquareHoleHoleSnapshot>,
|
||||
pub last_feedback: Option<SquareHoleDropFeedbackSnapshot>,
|
||||
|
||||
Reference in New Issue
Block a user