fix: 优化跳一跳运行态与地块资源
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -6,7 +6,9 @@ use crate::{
|
||||
};
|
||||
|
||||
const JUMP_HOP_PLATFORM_SIZE_MULTIPLIER: f32 = 2.0;
|
||||
const JUMP_HOP_CHARGE_TO_DISTANCE_RATIO: f32 = 0.008;
|
||||
const JUMP_HOP_CHARGE_TO_DISTANCE_RATIO: f32 = 0.004;
|
||||
const JUMP_HOP_TOP_FACE_HITBOX_WIDTH_RATIO: f32 = 0.72;
|
||||
const JUMP_HOP_TOP_FACE_HITBOX_HEIGHT_RATIO: f32 = 0.52;
|
||||
|
||||
pub fn generate_jump_hop_path(seed: &str, difficulty: JumpHopDifficulty) -> JumpHopPath {
|
||||
let config = difficulty_config(difficulty);
|
||||
@@ -62,8 +64,8 @@ pub fn start_run(
|
||||
pub fn apply_jump(
|
||||
run: &JumpHopRunSnapshot,
|
||||
drag_distance: f32,
|
||||
drag_vector_x: Option<f32>,
|
||||
drag_vector_y: Option<f32>,
|
||||
_drag_vector_x: Option<f32>,
|
||||
_drag_vector_y: Option<f32>,
|
||||
jumped_at_ms: u64,
|
||||
) -> Result<JumpHopRunSnapshot, JumpHopError> {
|
||||
if run.status != JumpHopRunStatus::Playing {
|
||||
@@ -86,20 +88,15 @@ pub fn apply_jump(
|
||||
let vector_x = target.x - current.x;
|
||||
let vector_y = target.y - current.y;
|
||||
let target_distance = vector_x.hypot(vector_y).max(0.0001);
|
||||
let (unit_x, unit_y) = normalize_jump_direction(
|
||||
drag_vector_x,
|
||||
drag_vector_y,
|
||||
vector_x / target_distance,
|
||||
vector_y / target_distance,
|
||||
);
|
||||
let unit_x = vector_x / target_distance;
|
||||
let unit_y = vector_y / target_distance;
|
||||
let landed_x = current.x + unit_x * jump_distance;
|
||||
let landed_y = current.y + unit_y * jump_distance;
|
||||
let landing_error = (landed_x - target.x).hypot(landed_y - target.y);
|
||||
let target_landing_radius = target.landing_radius;
|
||||
let landed_on_target = is_landing_inside_platform_footprint(target, landed_x, landed_y);
|
||||
|
||||
let mut next = run.clone();
|
||||
next.path = path;
|
||||
let result = if landing_error <= target_landing_radius {
|
||||
let result = if landed_on_target {
|
||||
JumpHopJumpResultKind::Hit
|
||||
} else {
|
||||
JumpHopJumpResultKind::Miss
|
||||
@@ -128,6 +125,19 @@ pub fn apply_jump(
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
fn is_landing_inside_platform_footprint(
|
||||
platform: &JumpHopPlatform,
|
||||
landed_x: f32,
|
||||
landed_y: f32,
|
||||
) -> bool {
|
||||
let half_width = (platform.width * 0.5 * JUMP_HOP_TOP_FACE_HITBOX_WIDTH_RATIO).max(0.0);
|
||||
let half_height = (platform.height * 0.5 * JUMP_HOP_TOP_FACE_HITBOX_HEIGHT_RATIO).max(0.0);
|
||||
let error_x = landed_x - platform.x;
|
||||
let error_y = landed_y - platform.y;
|
||||
|
||||
error_x.abs() <= half_width && error_y.abs() <= half_height
|
||||
}
|
||||
|
||||
pub fn restart_run(
|
||||
run: &JumpHopRunSnapshot,
|
||||
next_run_id: String,
|
||||
@@ -250,30 +260,6 @@ fn extend_jump_hop_path(mut path: JumpHopPath, required_count: usize) -> JumpHop
|
||||
path
|
||||
}
|
||||
|
||||
fn normalize_jump_direction(
|
||||
drag_vector_x: Option<f32>,
|
||||
drag_vector_y: Option<f32>,
|
||||
fallback_x: f32,
|
||||
fallback_y: f32,
|
||||
) -> (f32, f32) {
|
||||
let Some(drag_x) = drag_vector_x.filter(|value| value.is_finite()) else {
|
||||
return (fallback_x, fallback_y);
|
||||
};
|
||||
let Some(drag_y) = drag_vector_y.filter(|value| value.is_finite()) else {
|
||||
return (fallback_x, fallback_y);
|
||||
};
|
||||
// 前端提交的是屏幕拖拽向量:x 轴同向,y 轴向下为正。
|
||||
// 真实起跳需要“反向弹出”,同时把屏幕 y 翻回世界坐标的向上为正。
|
||||
let jump_x = -drag_x;
|
||||
let jump_y = drag_y;
|
||||
let length = jump_x.hypot(jump_y);
|
||||
if length < 0.0001 {
|
||||
(fallback_x, fallback_y)
|
||||
} else {
|
||||
(jump_x / length, jump_y / length)
|
||||
}
|
||||
}
|
||||
|
||||
fn difficulty_config(difficulty: JumpHopDifficulty) -> DifficultyConfig {
|
||||
match difficulty {
|
||||
JumpHopDifficulty::Easy => DifficultyConfig {
|
||||
@@ -353,8 +339,8 @@ impl DeterministicRng {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
JumpHopDifficulty, JumpHopJumpResultKind, JumpHopRunStatus, apply_jump,
|
||||
generate_jump_hop_path, restart_run, start_run,
|
||||
JumpHopDifficulty, JumpHopJumpResultKind, JumpHopPlatform, JumpHopRunStatus,
|
||||
JumpHopTileType, apply_jump, generate_jump_hop_path, restart_run, start_run,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -371,16 +357,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn difficulty_charge_to_distance_ratio_is_doubled() {
|
||||
fn difficulty_charge_to_distance_ratio_is_reduced_for_long_press() {
|
||||
let easy = generate_jump_hop_path("seed-ratio-easy", JumpHopDifficulty::Easy);
|
||||
let standard = generate_jump_hop_path("seed-ratio-standard", JumpHopDifficulty::Standard);
|
||||
let advanced = generate_jump_hop_path("seed-ratio-advanced", JumpHopDifficulty::Advanced);
|
||||
let challenge = generate_jump_hop_path("seed-ratio-challenge", JumpHopDifficulty::Challenge);
|
||||
let challenge =
|
||||
generate_jump_hop_path("seed-ratio-challenge", JumpHopDifficulty::Challenge);
|
||||
|
||||
assert_eq!(easy.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(standard.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(advanced.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(challenge.scoring.charge_to_distance_ratio, 0.008);
|
||||
assert_eq!(easy.scoring.charge_to_distance_ratio, 0.004);
|
||||
assert_eq!(standard.scoring.charge_to_distance_ratio, 0.004);
|
||||
assert_eq!(advanced.scoring.charge_to_distance_ratio, 0.004);
|
||||
assert_eq!(challenge.scoring.charge_to_distance_ratio, 0.004);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -454,7 +441,7 @@ mod tests {
|
||||
None,
|
||||
200,
|
||||
)
|
||||
.expect("jump should resolve");
|
||||
.expect("jump should resolve");
|
||||
assert_eq!(miss.status, JumpHopRunStatus::Failed);
|
||||
assert_eq!(
|
||||
miss.last_jump.as_ref().unwrap().result,
|
||||
@@ -463,7 +450,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_resolution_uses_screen_drag_y_axis_for_forward_jump_direction() {
|
||||
fn jump_resolution_ignores_client_drag_direction_and_targets_next_center() {
|
||||
let path = generate_jump_hop_path("seed-screen-axis", JumpHopDifficulty::Easy);
|
||||
let run = start_run(
|
||||
"run-screen-axis".to_string(),
|
||||
@@ -478,21 +465,49 @@ mod tests {
|
||||
let target_distance = (target.x - current.x).hypot(target.y - current.y);
|
||||
let charge = (target_distance / run.path.scoring.charge_to_distance_ratio).round() as u32;
|
||||
|
||||
let result = apply_jump(
|
||||
&run,
|
||||
charge as f32,
|
||||
Some(-(target.x - current.x)),
|
||||
Some(target.y - current.y),
|
||||
200,
|
||||
)
|
||||
.expect("jump should resolve");
|
||||
let result = apply_jump(&run, charge as f32, Some(-999.0), Some(-999.0), 200)
|
||||
.expect("jump should resolve");
|
||||
|
||||
let last_jump = result.last_jump.as_ref().expect("last jump should exist");
|
||||
assert_eq!(result.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(
|
||||
result.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Hit
|
||||
);
|
||||
assert_eq!(last_jump.result, JumpHopJumpResultKind::Hit);
|
||||
assert_eq!(result.current_platform_index, 1);
|
||||
assert!((last_jump.landed_x - target.x).abs() < target.landing_radius);
|
||||
assert!((last_jump.landed_y - target.y).abs() < target.landing_radius);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_resolution_uses_visual_top_face_footprint_instead_of_landing_radius() {
|
||||
let mut path = generate_jump_hop_path("seed-footprint", JumpHopDifficulty::Easy);
|
||||
path.platforms[0] = test_platform("p0", 0.0, 0.0, 1.2, 1.0);
|
||||
path.platforms[1] = test_platform("p1", 1.0, 0.0, 2.0, 0.6);
|
||||
path.scoring.max_charge_ms = 600;
|
||||
let run = start_run(
|
||||
"run-footprint".to_string(),
|
||||
"user-footprint".to_string(),
|
||||
"profile-footprint".to_string(),
|
||||
path,
|
||||
100,
|
||||
)
|
||||
.expect("run should start");
|
||||
|
||||
let edge_hit_charge = 1.6 / run.path.scoring.charge_to_distance_ratio;
|
||||
let edge_hit =
|
||||
apply_jump(&run, edge_hit_charge, None, None, 200).expect("jump should resolve");
|
||||
let last_hit = edge_hit.last_jump.as_ref().expect("last jump should exist");
|
||||
assert_eq!(edge_hit.status, JumpHopRunStatus::Playing);
|
||||
assert_eq!(last_hit.result, JumpHopJumpResultKind::Hit);
|
||||
assert!(last_hit.landed_x > 1.5);
|
||||
assert!(last_hit.landed_x <= 1.72);
|
||||
|
||||
let outside_charge = 1.8 / run.path.scoring.charge_to_distance_ratio;
|
||||
let outside =
|
||||
apply_jump(&run, outside_charge, None, None, 200).expect("jump should resolve");
|
||||
assert_eq!(outside.status, JumpHopRunStatus::Failed);
|
||||
assert_eq!(
|
||||
outside.last_jump.as_ref().unwrap().result,
|
||||
JumpHopJumpResultKind::Miss
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -551,4 +566,18 @@ mod tests {
|
||||
assert!(run.path.platforms.len() >= 12);
|
||||
assert!(run.finished_at_ms.is_none());
|
||||
}
|
||||
|
||||
fn test_platform(id: &str, x: f32, y: f32, width: f32, height: f32) -> JumpHopPlatform {
|
||||
JumpHopPlatform {
|
||||
platform_id: id.to_string(),
|
||||
tile_type: JumpHopTileType::Normal,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
landing_radius: 0.2,
|
||||
perfect_radius: 0.1,
|
||||
score_value: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use super::{
|
||||
},
|
||||
response::handle_vector_engine_response,
|
||||
types::{GeneratedImages, ReferenceImage, VectorEngineImageSettings},
|
||||
util::truncate_raw,
|
||||
};
|
||||
|
||||
pub async fn create_vector_engine_image_generation(
|
||||
@@ -66,7 +67,25 @@ pub async fn create_vector_engine_image_generation(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => break response,
|
||||
Ok(response) => {
|
||||
if should_retry_vector_engine_upstream_status(response.status, attempt) {
|
||||
retry_vector_engine_upstream_status_after_delay(
|
||||
"generation",
|
||||
request_url.as_str(),
|
||||
attempt,
|
||||
response.status,
|
||||
response.body.as_str(),
|
||||
started_at.elapsed().as_millis() as u64,
|
||||
Some(prompt.chars().count()),
|
||||
Some(reference_images.len()),
|
||||
Some(&request_body),
|
||||
)
|
||||
.await;
|
||||
attempt += 1;
|
||||
continue;
|
||||
}
|
||||
break response;
|
||||
}
|
||||
Err(error) => {
|
||||
if should_retry_vector_engine_curl_send_error(&error, attempt) {
|
||||
retry_vector_engine_send_after_delay(
|
||||
@@ -75,7 +94,7 @@ pub async fn create_vector_engine_image_generation(
|
||||
"request_send",
|
||||
attempt,
|
||||
error.is_timeout(),
|
||||
error.is_connect(),
|
||||
error.is_connect() || error.is_transient_transport(),
|
||||
true,
|
||||
false,
|
||||
error.to_string().as_str(),
|
||||
@@ -220,7 +239,25 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => break response,
|
||||
Ok(response) => {
|
||||
if should_retry_vector_engine_upstream_status(response.status, attempt) {
|
||||
retry_vector_engine_upstream_status_after_delay(
|
||||
"edit",
|
||||
request_url.as_str(),
|
||||
attempt,
|
||||
response.status,
|
||||
response.body.as_str(),
|
||||
started_at.elapsed().as_millis() as u64,
|
||||
Some(prompt.chars().count()),
|
||||
Some(reference_image_count),
|
||||
Some(&request_params),
|
||||
)
|
||||
.await;
|
||||
attempt += 1;
|
||||
continue;
|
||||
}
|
||||
break response;
|
||||
}
|
||||
Err(error) => {
|
||||
if should_retry_vector_engine_curl_send_error(&error, attempt) {
|
||||
retry_vector_engine_send_after_delay(
|
||||
@@ -229,7 +266,7 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
"request_send",
|
||||
attempt,
|
||||
error.is_timeout(),
|
||||
error.is_connect(),
|
||||
error.is_connect() || error.is_transient_transport(),
|
||||
true,
|
||||
false,
|
||||
error.to_string().as_str(),
|
||||
@@ -290,7 +327,12 @@ fn should_retry_vector_engine_curl_send_error(
|
||||
error: &super::curl_transport::VectorEngineCurlError,
|
||||
attempt: u32,
|
||||
) -> bool {
|
||||
attempt < VECTOR_ENGINE_SEND_MAX_ATTEMPTS && (error.is_timeout() || error.is_connect())
|
||||
attempt < VECTOR_ENGINE_SEND_MAX_ATTEMPTS
|
||||
&& (error.is_timeout() || error.is_connect() || error.is_transient_transport())
|
||||
}
|
||||
|
||||
fn should_retry_vector_engine_upstream_status(status: u16, attempt: u32) -> bool {
|
||||
attempt < VECTOR_ENGINE_SEND_MAX_ATTEMPTS && (status == 408 || status == 429 || status >= 500)
|
||||
}
|
||||
|
||||
async fn retry_vector_engine_send_after_delay(
|
||||
@@ -334,6 +376,40 @@ async fn retry_vector_engine_send_after_delay(
|
||||
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
|
||||
async fn retry_vector_engine_upstream_status_after_delay(
|
||||
request_kind: &'static str,
|
||||
request_url: &str,
|
||||
attempt: u32,
|
||||
status: u16,
|
||||
raw_body: &str,
|
||||
elapsed_ms: u64,
|
||||
prompt_chars: Option<usize>,
|
||||
reference_image_count: Option<usize>,
|
||||
request_params: Option<&serde_json::Value>,
|
||||
) {
|
||||
let delay_ms = vector_engine_send_retry_delay_ms(attempt, vector_engine_send_retry_jitter_ms());
|
||||
tracing::warn!(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
endpoint = %request_url,
|
||||
request_kind,
|
||||
failure_stage = "upstream_status",
|
||||
attempt,
|
||||
max_attempts = VECTOR_ENGINE_SEND_MAX_ATTEMPTS,
|
||||
retry_delay_ms = delay_ms,
|
||||
status,
|
||||
retryable = true,
|
||||
elapsed_ms,
|
||||
prompt_chars,
|
||||
reference_image_count,
|
||||
raw_excerpt = %truncate_raw(raw_body),
|
||||
request_params = %request_params
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_default(),
|
||||
"VectorEngine 图片上游状态可重试,准备重试"
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
|
||||
fn vector_engine_send_retry_delay_ms(attempt: u32, jitter_ms: u64) -> u64 {
|
||||
let exponential_factor = 1_u64 << attempt.saturating_sub(1).min(10);
|
||||
let bounded_jitter_ms = jitter_ms.min(VECTOR_ENGINE_SEND_RETRY_MAX_JITTER_MS);
|
||||
@@ -357,6 +433,33 @@ mod tests {
|
||||
assert_eq!(VECTOR_ENGINE_SEND_MAX_ATTEMPTS, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_send_retry_policy_treats_ssl_reset_as_transient_transport() {
|
||||
let error = super::super::curl_transport::VectorEngineCurlError::Curl(curl::Error::new(35));
|
||||
|
||||
assert!(error.is_transient_transport());
|
||||
assert!(should_retry_vector_engine_curl_send_error(&error, 1));
|
||||
assert!(!should_retry_vector_engine_curl_send_error(&error, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_send_retry_policy_treats_recv_eof_as_transient_transport() {
|
||||
let error = super::super::curl_transport::VectorEngineCurlError::Curl(curl::Error::new(56));
|
||||
|
||||
assert!(error.is_transient_transport());
|
||||
assert!(should_retry_vector_engine_curl_send_error(&error, 1));
|
||||
assert!(!should_retry_vector_engine_curl_send_error(&error, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_send_retry_policy_treats_upstream_502_as_retryable() {
|
||||
assert!(should_retry_vector_engine_upstream_status(502, 1));
|
||||
assert!(should_retry_vector_engine_upstream_status(429, 1));
|
||||
assert!(should_retry_vector_engine_upstream_status(408, 1));
|
||||
assert!(!should_retry_vector_engine_upstream_status(400, 1));
|
||||
assert!(!should_retry_vector_engine_upstream_status(502, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_send_retry_delay_uses_exponential_backoff_with_bounded_jitter() {
|
||||
assert_eq!(vector_engine_send_retry_delay_ms(1, 0), 500);
|
||||
|
||||
@@ -45,6 +45,25 @@ impl VectorEngineCurlError {
|
||||
Self::Form(_) | Self::WorkerJoin(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_transient_transport(&self) -> bool {
|
||||
match self {
|
||||
Self::Curl(error) => {
|
||||
let message = error.to_string().to_ascii_lowercase();
|
||||
error.is_ssl_connect_error()
|
||||
|| error.is_recv_error()
|
||||
|| error.is_send_error()
|
||||
|| message.contains("connection reset")
|
||||
|| message.contains("recv failure")
|
||||
|| message.contains("receive failure")
|
||||
|| message.contains("receiving data")
|
||||
|| message.contains("unexpected eof")
|
||||
|| message.contains("send failure")
|
||||
|| message.contains("broken pipe")
|
||||
}
|
||||
Self::Form(_) | Self::WorkerJoin(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VectorEngineCurlError {
|
||||
@@ -136,7 +155,7 @@ pub(crate) fn map_curl_error(
|
||||
request_params: Option<&Value>,
|
||||
) -> PlatformImageError {
|
||||
let is_timeout = error.is_timeout();
|
||||
let is_connect = error.is_connect();
|
||||
let is_connect = error.is_connect() || error.is_transient_transport();
|
||||
let source = error.to_string();
|
||||
let message = format!("{context}:{source}");
|
||||
let audit = build_failure_audit(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use platform_image::vector_engine::{
|
||||
GPT_IMAGE_2_MODEL, ReferenceImage, VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings,
|
||||
build_vector_engine_image_http_client, build_vector_engine_image_request_body,
|
||||
create_vector_engine_image_edit, vector_engine_images_edit_url,
|
||||
vector_engine_images_generation_url,
|
||||
create_vector_engine_image_edit, create_vector_engine_image_generation,
|
||||
vector_engine_images_edit_url, vector_engine_images_generation_url,
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
@@ -109,3 +109,72 @@ async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() {
|
||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn vector_engine_image_generation_retries_upstream_502_once_and_succeeds() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("mock server should bind");
|
||||
let server_addr = listener
|
||||
.local_addr()
|
||||
.expect("mock server address should be readable");
|
||||
let request_count = Arc::new(AtomicUsize::new(0));
|
||||
let request_count_for_server = Arc::clone(&request_count);
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
loop {
|
||||
let Ok((mut stream, _)) = listener.accept().await else {
|
||||
break;
|
||||
};
|
||||
let request_index = request_count_for_server.fetch_add(1, Ordering::SeqCst);
|
||||
tokio::spawn(async move {
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let _ = stream.read(&mut buffer).await;
|
||||
if request_index == 0 {
|
||||
let body = "<html><head><title>502 Bad Gateway</title></head><body><center><h1>502 Bad Gateway</h1></center><hr><center>nginx</center></body></html>";
|
||||
let response = format!(
|
||||
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(response.as_bytes()).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let body = r#"{"data":[{"b64_json":"iVBORw0KGgpyZXN0"}]}"#;
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
let _ = stream.write_all(response.as_bytes()).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let settings = VectorEngineImageSettings {
|
||||
base_url: format!("http://{server_addr}/v1"),
|
||||
api_key: "test-key".to_string(),
|
||||
request_timeout_ms: 1_000,
|
||||
};
|
||||
let http_client =
|
||||
build_vector_engine_image_http_client(&settings).expect("client should build");
|
||||
|
||||
let generated = create_vector_engine_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
"测试提示词",
|
||||
None,
|
||||
"1024x1024",
|
||||
1,
|
||||
&[],
|
||||
"测试 VectorEngine 图片生成失败",
|
||||
)
|
||||
.await
|
||||
.expect("second attempt should return generated image");
|
||||
|
||||
assert_eq!(generated.images.len(), 1);
|
||||
assert_eq!(generated.images[0].mime_type, "image/png");
|
||||
assert_eq!(request_count.load(Ordering::SeqCst), 2);
|
||||
server.abort();
|
||||
}
|
||||
|
||||
@@ -166,6 +166,45 @@ pub struct JumpHopTileAsset {
|
||||
pub visual_height: u32,
|
||||
pub top_surface_radius: f32,
|
||||
pub landing_radius: f32,
|
||||
#[serde(default)]
|
||||
pub face_assets: Option<JumpHopTileFaceAssets>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum JumpHopTileFaceKey {
|
||||
Top,
|
||||
Front,
|
||||
Right,
|
||||
Back,
|
||||
Left,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JumpHopTileFaceAsset {
|
||||
pub face: JumpHopTileFaceKey,
|
||||
pub asset_id: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: String,
|
||||
pub asset_object_id: String,
|
||||
pub generation_provider: String,
|
||||
pub prompt: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub source_atlas_cell: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JumpHopTileFaceAssets {
|
||||
pub top: JumpHopTileFaceAsset,
|
||||
pub front: JumpHopTileFaceAsset,
|
||||
pub right: JumpHopTileFaceAsset,
|
||||
pub back: JumpHopTileFaceAsset,
|
||||
pub left: JumpHopTileFaceAsset,
|
||||
pub bottom: JumpHopTileFaceAsset,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@@ -473,9 +473,9 @@ fn validate_jump_hop_runtime_ready(
|
||||
}
|
||||
validate_jump_hop_default_character_ready(work)?;
|
||||
validate_jump_hop_tile_atlas_asset_ready(&work.tile_atlas_asset, "tile_atlas_asset")?;
|
||||
if work.tile_assets.len() < 25 {
|
||||
if work.tile_assets.len() < 18 {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop runtime 需要 25 个地块资产",
|
||||
"jump-hop runtime 需要 18 个地块资产",
|
||||
));
|
||||
}
|
||||
for (index, asset) in work.tile_assets.iter().enumerate() {
|
||||
@@ -761,12 +761,12 @@ fn build_compile_input(
|
||||
draft.default_character = Some(default_jump_hop_default_character());
|
||||
let tile_atlas_asset = draft.tile_atlas_asset.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 缺少真实地块图集资产,请先由 api-server 生成并持久化 asset_object",
|
||||
"jump-hop compile-draft 缺少真实地板贴图图集资产,请先由 api-server 生成并持久化 asset_object",
|
||||
)
|
||||
})?;
|
||||
let tile_assets = if draft.tile_assets.len() < 25 {
|
||||
let tile_assets = if draft.tile_assets.len() < 18 {
|
||||
return Err(SpacetimeClientError::validation_failed(
|
||||
"jump-hop compile-draft 需要 25 个真实地块资产,请先由 api-server 生成并持久化 asset_object",
|
||||
"jump-hop compile-draft 需要 18 个真实地块资产,请先由 api-server 生成并持久化 asset_object",
|
||||
));
|
||||
} else {
|
||||
draft.tile_assets.clone()
|
||||
@@ -878,7 +878,7 @@ fn default_draft() -> JumpHopDraftResponse {
|
||||
style_preset: JumpHopStylePreset::MinimalBlocks,
|
||||
default_character: Some(default_jump_hop_default_character()),
|
||||
character_prompt: "内置默认 3D 角色".to_string(),
|
||||
tile_prompt: "跳一跳主题的正面30度视角主题物体图集,物体本身作为跳跃落点".to_string(),
|
||||
tile_prompt: "跳一跳主题的3D立方体主题身份方块包装图集".to_string(),
|
||||
end_mood_prompt: None,
|
||||
character_asset: None,
|
||||
tile_atlas_asset: None,
|
||||
@@ -994,7 +994,7 @@ mod tests {
|
||||
const NOW_MICROS: i64 = 1_763_456_789_000_000;
|
||||
|
||||
#[test]
|
||||
fn jump_hop_action_compile_draft_builds_compile_input_with_25_tile_assets_and_builtin_character()
|
||||
fn jump_hop_action_compile_draft_builds_compile_input_with_18_tile_assets_and_builtin_character()
|
||||
{
|
||||
let session = session_with_draft(draft_without_character_asset());
|
||||
let payload = action(JumpHopActionType::CompileDraft);
|
||||
@@ -1028,9 +1028,9 @@ mod tests {
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("old-tile-25-object")
|
||||
.contains("old-tile-18-object")
|
||||
);
|
||||
assert_eq!(draft.tile_assets.len(), 25);
|
||||
assert_eq!(draft.tile_assets.len(), 18);
|
||||
assert_eq!(draft.generation_status, JumpHopGenerationStatus::Ready);
|
||||
}
|
||||
|
||||
@@ -1040,7 +1040,7 @@ mod tests {
|
||||
let mut payload = action(JumpHopActionType::RegenerateTiles);
|
||||
payload.tile_prompt = Some("新的地块提示词".to_string());
|
||||
payload.tile_atlas_asset = Some(tile_atlas_asset("new-tile-atlas", NOW_MICROS));
|
||||
payload.tile_assets = Some(tile_assets("new", 25));
|
||||
payload.tile_assets = Some(tile_assets("new", 18));
|
||||
|
||||
let (plan, _draft) =
|
||||
build_jump_hop_action_plan(&session, OWNER_USER_ID, &payload, NOW_MICROS)
|
||||
@@ -1082,7 +1082,7 @@ mod tests {
|
||||
.tile_assets_json
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.contains("new-tile-25-object")
|
||||
.contains("new-tile-18-object")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1196,7 +1196,7 @@ mod tests {
|
||||
JumpHopDraftResponse {
|
||||
profile_id: None,
|
||||
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
|
||||
tile_assets: tile_assets("old", 25),
|
||||
tile_assets: tile_assets("old", 18),
|
||||
..base_draft()
|
||||
}
|
||||
}
|
||||
@@ -1206,7 +1206,7 @@ mod tests {
|
||||
profile_id: Some(PROFILE_ID.to_string()),
|
||||
character_asset: Some(build_jump_hop_default_character_asset(PROFILE_ID, "旧主题")),
|
||||
tile_atlas_asset: Some(tile_atlas_asset("old-tile-atlas", 0)),
|
||||
tile_assets: tile_assets("old", 25),
|
||||
tile_assets: tile_assets("old", 18),
|
||||
path: Some(sample_jump_hop_path()),
|
||||
cover_composite: Some("/generated-jump-hop-assets/old-cover.png".to_string()),
|
||||
generation_status: JumpHopGenerationStatus::Ready,
|
||||
@@ -1243,13 +1243,14 @@ mod tests {
|
||||
index + 1
|
||||
),
|
||||
asset_object_id: format!("{prefix}-tile-{:02}-object", index + 1),
|
||||
source_atlas_cell: format!("row-{}-col-{}", index / 5 + 1, index % 5 + 1),
|
||||
atlas_row: Some(index as u32 / 5 + 1),
|
||||
atlas_col: Some(index as u32 % 5 + 1),
|
||||
source_atlas_cell: format!("row-{}-col-{}", index / 3 + 1, index % 3 + 1),
|
||||
atlas_row: Some(index as u32 / 3 + 1),
|
||||
atlas_col: Some(index as u32 % 3 + 1),
|
||||
visual_width: 256,
|
||||
visual_height: 192,
|
||||
top_surface_radius: 42.0,
|
||||
landing_radius: 34.0,
|
||||
face_assets: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ pub use shared_contracts::jump_hop::{
|
||||
JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus,
|
||||
JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse,
|
||||
JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset,
|
||||
JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
|
||||
JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
JumpHopTileFaceAsset, JumpHopTileFaceAssets, JumpHopTileFaceKey, JumpHopTileType,
|
||||
JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorkProfileResponse,
|
||||
JumpHopWorkSummaryResponse, JumpHopWorksResponse, JumpHopWorkspaceCreateRequest,
|
||||
};
|
||||
|
||||
pub(crate) fn map_jump_hop_agent_session_procedure_result(
|
||||
@@ -267,6 +267,33 @@ fn map_tile_asset(snapshot: JumpHopTileAssetSnapshot) -> JumpHopTileAsset {
|
||||
visual_height: snapshot.visual_height,
|
||||
top_surface_radius: snapshot.top_surface_radius,
|
||||
landing_radius: snapshot.landing_radius,
|
||||
face_assets: snapshot.face_assets.map(map_tile_face_assets),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tile_face_assets(snapshot: JumpHopTileFaceAssetsSnapshot) -> JumpHopTileFaceAssets {
|
||||
JumpHopTileFaceAssets {
|
||||
top: map_tile_face_asset(snapshot.top),
|
||||
front: map_tile_face_asset(snapshot.front),
|
||||
right: map_tile_face_asset(snapshot.right),
|
||||
back: map_tile_face_asset(snapshot.back),
|
||||
left: map_tile_face_asset(snapshot.left),
|
||||
bottom: map_tile_face_asset(snapshot.bottom),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_tile_face_asset(snapshot: JumpHopTileFaceAssetSnapshot) -> JumpHopTileFaceAsset {
|
||||
JumpHopTileFaceAsset {
|
||||
face: parse_tile_face_key(&snapshot.face),
|
||||
asset_id: snapshot.asset_id,
|
||||
image_src: snapshot.image_src,
|
||||
image_object_key: snapshot.image_object_key,
|
||||
asset_object_id: snapshot.asset_object_id,
|
||||
generation_provider: snapshot.generation_provider,
|
||||
prompt: snapshot.prompt,
|
||||
width: snapshot.width,
|
||||
height: snapshot.height,
|
||||
source_atlas_cell: snapshot.source_atlas_cell,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,6 +432,17 @@ fn parse_tile_type(value: &str) -> JumpHopTileType {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tile_face_key(value: &str) -> JumpHopTileFaceKey {
|
||||
match value {
|
||||
"front" => JumpHopTileFaceKey::Front,
|
||||
"right" => JumpHopTileFaceKey::Right,
|
||||
"back" => JumpHopTileFaceKey::Back,
|
||||
"left" => JumpHopTileFaceKey::Left,
|
||||
"bottom" => JumpHopTileFaceKey::Bottom,
|
||||
_ => JumpHopTileFaceKey::Top,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_domain_tile_type(value: crate::module_bindings::JumpHopTileType) -> JumpHopTileType {
|
||||
match value {
|
||||
crate::module_bindings::JumpHopTileType::Start => JumpHopTileType::Start,
|
||||
|
||||
@@ -463,6 +463,8 @@ pub mod jump_hop_runtime_run_row_type;
|
||||
pub mod jump_hop_runtime_run_table;
|
||||
pub mod jump_hop_scoring_type;
|
||||
pub mod jump_hop_tile_asset_snapshot_type;
|
||||
pub mod jump_hop_tile_face_asset_snapshot_type;
|
||||
pub mod jump_hop_tile_face_assets_snapshot_type;
|
||||
pub mod jump_hop_tile_type_type;
|
||||
pub mod jump_hop_work_delete_input_type;
|
||||
pub mod jump_hop_work_get_input_type;
|
||||
@@ -1567,6 +1569,8 @@ pub use jump_hop_runtime_run_row_type::JumpHopRuntimeRunRow;
|
||||
pub use jump_hop_runtime_run_table::*;
|
||||
pub use jump_hop_scoring_type::JumpHopScoring;
|
||||
pub use jump_hop_tile_asset_snapshot_type::JumpHopTileAssetSnapshot;
|
||||
pub use jump_hop_tile_face_asset_snapshot_type::JumpHopTileFaceAssetSnapshot;
|
||||
pub use jump_hop_tile_face_assets_snapshot_type::JumpHopTileFaceAssetsSnapshot;
|
||||
pub use jump_hop_tile_type_type::JumpHopTileType;
|
||||
pub use jump_hop_work_delete_input_type::JumpHopWorkDeleteInput;
|
||||
pub use jump_hop_work_get_input_type::JumpHopWorkGetInput;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::jump_hop_tile_face_assets_snapshot_type::JumpHopTileFaceAssetsSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct JumpHopTileAssetSnapshot {
|
||||
@@ -19,6 +21,7 @@ pub struct JumpHopTileAssetSnapshot {
|
||||
pub visual_height: u32,
|
||||
pub top_surface_radius: f32,
|
||||
pub landing_radius: f32,
|
||||
pub face_assets: Option<JumpHopTileFaceAssetsSnapshot>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for JumpHopTileAssetSnapshot {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 JumpHopTileFaceAssetSnapshot {
|
||||
pub face: String,
|
||||
pub asset_id: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: String,
|
||||
pub asset_object_id: String,
|
||||
pub generation_provider: String,
|
||||
pub prompt: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub source_atlas_cell: String,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for JumpHopTileFaceAssetSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
use super::jump_hop_tile_face_asset_snapshot_type::JumpHopTileFaceAssetSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct JumpHopTileFaceAssetsSnapshot {
|
||||
pub top: JumpHopTileFaceAssetSnapshot,
|
||||
pub front: JumpHopTileFaceAssetSnapshot,
|
||||
pub right: JumpHopTileFaceAssetSnapshot,
|
||||
pub back: JumpHopTileFaceAssetSnapshot,
|
||||
pub left: JumpHopTileFaceAssetSnapshot,
|
||||
pub bottom: JumpHopTileFaceAssetSnapshot,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for JumpHopTileFaceAssetsSnapshot {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -1311,7 +1311,7 @@ fn default_config_from_seed(seed_text: &str) -> JumpHopCreatorConfigSnapshot {
|
||||
difficulty: JumpHopDifficulty::Standard.as_str().to_string(),
|
||||
style_preset: JUMP_HOP_STYLE_MINIMAL_BLOCKS.to_string(),
|
||||
character_prompt: "内置默认 3D 角色".to_string(),
|
||||
tile_prompt: format!("{seed}主题的正面30度视角主题物体图集,物体本身作为跳跃落点"),
|
||||
tile_prompt: format!("{seed}主题的3D立方体主题身份方块包装图集"),
|
||||
end_mood_prompt: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,34 @@ pub struct JumpHopTileAssetSnapshot {
|
||||
pub visual_height: u32,
|
||||
pub top_surface_radius: f32,
|
||||
pub landing_radius: f32,
|
||||
#[serde(default)]
|
||||
pub face_assets: Option<JumpHopTileFaceAssetsSnapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JumpHopTileFaceAssetSnapshot {
|
||||
pub face: String,
|
||||
pub asset_id: String,
|
||||
pub image_src: String,
|
||||
pub image_object_key: String,
|
||||
pub asset_object_id: String,
|
||||
pub generation_provider: String,
|
||||
pub prompt: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub source_atlas_cell: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JumpHopTileFaceAssetsSnapshot {
|
||||
pub top: JumpHopTileFaceAssetSnapshot,
|
||||
pub front: JumpHopTileFaceAssetSnapshot,
|
||||
pub right: JumpHopTileFaceAssetSnapshot,
|
||||
pub back: JumpHopTileFaceAssetSnapshot,
|
||||
pub left: JumpHopTileFaceAssetSnapshot,
|
||||
pub bottom: JumpHopTileFaceAssetSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
|
||||
Reference in New Issue
Block a user