1
This commit is contained in:
@@ -13,13 +13,12 @@ use module_match3d::{
|
||||
Match3DItemSnapshot as DomainMatch3DItemSnapshot, Match3DItemState as DomainMatch3DItemState,
|
||||
Match3DRunSnapshot as DomainMatch3DRunSnapshot, Match3DRunStatus as DomainMatch3DRunStatus,
|
||||
Match3DTraySlot as DomainMatch3DTraySlot, confirm_click_at as confirm_domain_click_at,
|
||||
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_with_seed_at,
|
||||
resolve_run_timer_at as resolve_domain_run_timer_at, start_run_with_seed_at_and_item_type_count,
|
||||
stop_run_at as stop_domain_run_at,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
const MATCH3D_GENERATED_ITEM_COUNT_MVP: u32 = 3;
|
||||
use serde_json::Value;
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_match3d_agent_session(
|
||||
@@ -722,7 +721,12 @@ fn start_match3d_run_tx(
|
||||
} else {
|
||||
current_server_ms(ctx)
|
||||
};
|
||||
let mut snapshot = build_initial_run_snapshot(&input.run_id, &work, started_at_ms);
|
||||
let mut snapshot = build_initial_run_snapshot(
|
||||
&input.run_id,
|
||||
&work,
|
||||
started_at_ms,
|
||||
normalize_match3d_item_type_count_override(input.item_type_count_override),
|
||||
);
|
||||
snapshot.server_now_ms = current_server_ms(ctx);
|
||||
snapshot.remaining_ms = compute_remaining_ms(&snapshot, snapshot.server_now_ms);
|
||||
let now = ctx.timestamp;
|
||||
@@ -838,6 +842,7 @@ fn restart_match3d_run_tx(
|
||||
input: Match3DRunRestartInput,
|
||||
) -> Result<Match3DRunSnapshot, String> {
|
||||
let source = find_owned_run(ctx, &input.source_run_id, &input.owner_user_id)?;
|
||||
let item_type_count_override = resolve_item_type_count_override_from_run(&source);
|
||||
start_match3d_run_tx(
|
||||
ctx,
|
||||
Match3DRunStartInput {
|
||||
@@ -845,6 +850,7 @@ fn restart_match3d_run_tx(
|
||||
owner_user_id: input.owner_user_id,
|
||||
profile_id: source.profile_id,
|
||||
started_at_ms: input.restarted_at_ms,
|
||||
item_type_count_override,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -992,19 +998,25 @@ fn build_initial_run_snapshot(
|
||||
run_id: &str,
|
||||
work: &Match3DWorkProfileRow,
|
||||
started_at_ms: i64,
|
||||
item_type_count_override: Option<u32>,
|
||||
) -> Match3DRunSnapshot {
|
||||
let config = parse_config_or_default(&work.config_json);
|
||||
let domain_config =
|
||||
let mut domain_config =
|
||||
domain_config_from_snapshot(&config).unwrap_or_else(|_| fallback_domain_config());
|
||||
domain_config.clear_count = module_match3d::normalize_match3d_runtime_clear_count(
|
||||
domain_config.clear_count,
|
||||
domain_config.difficulty,
|
||||
);
|
||||
let domain_started_at_ms = to_u64_ms(started_at_ms);
|
||||
let seed = deterministic_run_seed(run_id, &work.profile_id, work.clear_count, work.difficulty);
|
||||
let domain_run = start_run_with_seed_at(
|
||||
let domain_run = start_run_with_seed_at_and_item_type_count(
|
||||
run_id.to_string(),
|
||||
work.owner_user_id.clone(),
|
||||
work.profile_id.clone(),
|
||||
&domain_config,
|
||||
seed,
|
||||
domain_started_at_ms,
|
||||
item_type_count_override,
|
||||
)
|
||||
.unwrap_or_else(|_| DomainMatch3DRunSnapshot {
|
||||
run_id: run_id.to_string(),
|
||||
@@ -1026,6 +1038,26 @@ fn build_initial_run_snapshot(
|
||||
snapshot_from_domain(&domain_run, started_at_ms)
|
||||
}
|
||||
|
||||
fn normalize_match3d_item_type_count_override(value: u32) -> Option<u32> {
|
||||
(value > 0).then_some(value)
|
||||
}
|
||||
|
||||
fn resolve_item_type_count_override_from_run(row: &Match3DRuntimeRunRow) -> u32 {
|
||||
deserialize_snapshot(&row.snapshot_json)
|
||||
.ok()
|
||||
.map(|snapshot| {
|
||||
let mut item_type_ids = snapshot
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.item_type_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
item_type_ids.sort();
|
||||
item_type_ids.dedup();
|
||||
item_type_ids.len() as u32
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn fallback_domain_config() -> DomainMatch3DCreatorConfig {
|
||||
DomainMatch3DCreatorConfig {
|
||||
theme_text: "经典消除".to_string(),
|
||||
@@ -1218,7 +1250,19 @@ fn validate_publishable_work(row: &Match3DWorkProfileRow) -> Result<(), String>
|
||||
if parse_tags(&row.tags_json)?.is_empty() {
|
||||
return Err("match3d 发布需要至少 1 个标签".to_string());
|
||||
}
|
||||
validate_config(&parse_config(&row.config_json)?)
|
||||
let config = parse_config(&row.config_json)?;
|
||||
let required_item_types =
|
||||
module_match3d::resolve_match3d_item_type_count_for_difficulty(
|
||||
config.clear_count,
|
||||
config.difficulty,
|
||||
) as usize;
|
||||
let ready_item_types = count_ready_generated_item_types(row.generated_item_assets_json.as_deref())?;
|
||||
if ready_item_types < required_item_types {
|
||||
return Err(format!(
|
||||
"match3d 发布需要至少 {required_item_types} 种物品素材,当前已有 {ready_item_types} 种"
|
||||
));
|
||||
}
|
||||
validate_config(&config)
|
||||
}
|
||||
|
||||
fn is_work_publish_ready(row: &Match3DWorkProfileRow) -> bool {
|
||||
@@ -1234,6 +1278,7 @@ fn default_config_from_seed(seed_text: &str) -> Match3DCreatorConfigSnapshot {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1255,10 +1300,8 @@ fn parse_config(value: &str) -> Result<Match3DCreatorConfigSnapshot, String> {
|
||||
}
|
||||
|
||||
fn normalize_match3d_generated_item_config(
|
||||
mut config: Match3DCreatorConfigSnapshot,
|
||||
config: Match3DCreatorConfigSnapshot,
|
||||
) -> Match3DCreatorConfigSnapshot {
|
||||
// 中文注释:素材生成首版任意难度都只生成 3 件物品,草稿编译也同步收敛。
|
||||
config.clear_count = MATCH3D_GENERATED_ITEM_COUNT_MVP;
|
||||
config
|
||||
}
|
||||
|
||||
@@ -1284,6 +1327,59 @@ fn normalize_generated_item_assets_json(value: Option<&str>) -> Result<Option<St
|
||||
Ok(Some(to_json_string(&parsed)))
|
||||
}
|
||||
|
||||
fn count_ready_generated_item_types(value: Option<&str>) -> Result<usize, String> {
|
||||
let Some(trimmed) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(0);
|
||||
};
|
||||
let parsed = parse_json::<Vec<Value>>(trimmed, "match3d generated_item_assets_json")?;
|
||||
Ok(parsed
|
||||
.iter()
|
||||
.filter(|asset| {
|
||||
let status_ready = asset
|
||||
.get("status")
|
||||
.and_then(Value::as_str)
|
||||
.map(|status| matches!(status, "image_ready" | "model_ready"))
|
||||
.unwrap_or(false);
|
||||
let view_count = asset
|
||||
.get("imageViews")
|
||||
.or_else(|| asset.get("image_views"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|views| {
|
||||
views
|
||||
.iter()
|
||||
.filter(|view| {
|
||||
view.get("imageSrc")
|
||||
.or_else(|| view.get("image_src"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
|| view
|
||||
.get("imageObjectKey")
|
||||
.or_else(|| view.get("image_object_key"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let has_primary_image = asset
|
||||
.get("imageSrc")
|
||||
.or_else(|| asset.get("image_src"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
|| asset
|
||||
.get("imageObjectKey")
|
||||
.or_else(|| asset.get("image_object_key"))
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| !value.trim().is_empty())
|
||||
.unwrap_or(false);
|
||||
status_ready && (view_count >= 5 || has_primary_image)
|
||||
})
|
||||
.count())
|
||||
}
|
||||
|
||||
fn resolve_generated_item_assets_json_for_compile(
|
||||
input: Option<&str>,
|
||||
existing_work: Option<&Match3DWorkProfileRow>,
|
||||
@@ -1695,6 +1791,7 @@ mod tests {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 0,
|
||||
@@ -1702,7 +1799,7 @@ mod tests {
|
||||
published_at: None,
|
||||
generated_item_assets_json: None,
|
||||
};
|
||||
let snapshot = build_initial_run_snapshot("run-1", &work, 10);
|
||||
let snapshot = build_initial_run_snapshot("run-1", &work, 10, None);
|
||||
assert_eq!(snapshot.total_item_count, 12);
|
||||
assert_eq!(snapshot.items.len(), 12);
|
||||
}
|
||||
@@ -1730,6 +1827,7 @@ mod tests {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 0,
|
||||
@@ -1774,6 +1872,7 @@ mod tests {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 2,
|
||||
@@ -1817,6 +1916,7 @@ mod tests {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: false,
|
||||
}),
|
||||
publication_status: MATCH3D_PUBLICATION_DRAFT.to_string(),
|
||||
play_count: 2,
|
||||
@@ -1846,7 +1946,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match3d_compile_normalizes_clear_count_to_three_item_mvp() {
|
||||
fn match3d_compile_keeps_difficulty_clear_count() {
|
||||
let config = normalize_match3d_generated_item_config(Match3DCreatorConfigSnapshot {
|
||||
theme_text: "水果".to_string(),
|
||||
reference_image_src: None,
|
||||
@@ -1855,10 +1955,12 @@ mod tests {
|
||||
asset_style_id: None,
|
||||
asset_style_label: None,
|
||||
asset_style_prompt: None,
|
||||
generate_click_sound: true,
|
||||
});
|
||||
|
||||
assert_eq!(config.clear_count, MATCH3D_GENERATED_ITEM_COUNT_MVP);
|
||||
assert_eq!(config.clear_count, 20);
|
||||
assert_eq!(config.difficulty, 8);
|
||||
assert!(config.generate_click_sound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user