fix: polish bark battle creation flow
This commit is contained in:
@@ -1001,6 +1001,7 @@ mod tests {
|
||||
"/generated-puzzle-assets/session-1/candidate/image.png",
|
||||
"/generated-custom-world-scenes/world-1/camp/scene.png",
|
||||
"/generated-custom-world-covers/world-1/cover.webp",
|
||||
"/generated-bark-battle-assets/draft/player/image.webp",
|
||||
"/generated-qwen-sprites/master/candidate-01.png",
|
||||
] {
|
||||
let response = app
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -174,6 +174,25 @@ mod tests {
|
||||
assert_eq!(resolve_creation_entry_route_id("/healthz"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creation_entry_config_response_opens_bark_battle() {
|
||||
let config = test_creation_entry_config_response();
|
||||
let bark_battle = config
|
||||
.creation_types
|
||||
.iter()
|
||||
.find(|item| item.id == "bark-battle")
|
||||
.expect("test creation entry config should include bark-battle");
|
||||
|
||||
assert_eq!(bark_battle.title, "汪汪声浪");
|
||||
assert!(bark_battle.visible);
|
||||
assert!(bark_battle.open);
|
||||
assert_eq!(bark_battle.badge, "可创建");
|
||||
assert_eq!(
|
||||
bark_battle.image_src,
|
||||
"/creation-type-references/bark-battle.webp"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_creation_entry_config_response_keeps_baby_object_match_coming_soon() {
|
||||
let config = test_creation_entry_config_response();
|
||||
|
||||
@@ -532,7 +532,9 @@ fn build_config_from_message(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_config_or_default(config: Option<&Match3DCreatorConfigRecord>) -> Match3DConfigJson {
|
||||
pub(super) fn resolve_config_or_default(
|
||||
config: Option<&Match3DCreatorConfigRecord>,
|
||||
) -> Match3DConfigJson {
|
||||
config
|
||||
.map(|config| Match3DConfigJson {
|
||||
theme_text: config.theme_text.clone(),
|
||||
@@ -595,7 +597,10 @@ fn build_match3d_assistant_reply(config: &Match3DConfigJson) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_assistant_reply_for_turn(config: &Match3DConfigJson, current_turn: u32) -> String {
|
||||
pub(super) fn build_match3d_assistant_reply_for_turn(
|
||||
config: &Match3DConfigJson,
|
||||
current_turn: u32,
|
||||
) -> String {
|
||||
match current_turn {
|
||||
0 => MATCH3D_QUESTION_THEME.to_string(),
|
||||
1 => MATCH3D_QUESTION_CLEAR_COUNT.to_string(),
|
||||
|
||||
@@ -1040,7 +1040,10 @@ pub(super) fn build_fallback_match3d_background_prompt(config: &Match3DConfigJso
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_fallback_match3d_item_sound_prompt(config: &Match3DConfigJson, item_name: &str) -> String {
|
||||
pub(super) fn build_fallback_match3d_item_sound_prompt(
|
||||
config: &Match3DConfigJson,
|
||||
item_name: &str,
|
||||
) -> String {
|
||||
let theme = config.theme_text.trim();
|
||||
let normalized_theme = if theme.is_empty() { "抓大鹅" } else { theme };
|
||||
normalize_match3d_audio_prompt(
|
||||
@@ -1416,7 +1419,9 @@ fn resolve_match3d_material_cell_crop(
|
||||
crop.to_crop_tuple()
|
||||
}
|
||||
|
||||
pub(super) fn crop_match3d_material_view_edge_matte(image: image::DynamicImage) -> image::DynamicImage {
|
||||
pub(super) fn crop_match3d_material_view_edge_matte(
|
||||
image: image::DynamicImage,
|
||||
) -> image::DynamicImage {
|
||||
let mut image = image.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
remove_match3d_material_view_edge_matte(image.as_mut(), width as usize, height as usize);
|
||||
|
||||
@@ -134,12 +134,11 @@ pub(super) fn map_match3d_draft_response(
|
||||
draft: Match3DResultDraftRecord,
|
||||
) -> Match3DResultDraftResponse {
|
||||
// 中文注释:session draft 自身也可能携带生成素材快照,不能只依赖 work detail 回读补齐 UI 背景和容器图。
|
||||
let generated_item_assets = parse_match3d_generated_item_assets(
|
||||
draft.generated_item_assets_json.as_deref(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(Match3DGeneratedItemAsset::from)
|
||||
.collect::<Vec<_>>();
|
||||
let generated_item_assets =
|
||||
parse_match3d_generated_item_assets(draft.generated_item_assets_json.as_deref())
|
||||
.into_iter()
|
||||
.map(Match3DGeneratedItemAsset::from)
|
||||
.collect::<Vec<_>>();
|
||||
let background_asset = find_match3d_generated_background_asset(&generated_item_assets);
|
||||
let mut response = Match3DResultDraftResponse {
|
||||
profile_id: draft.profile_id,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -587,7 +587,10 @@ async fn load_match3d_container_reference_image() -> Result<OpenAiReferenceImage
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_background_generation_prompt(config: &Match3DConfigJson, prompt: &str) -> String {
|
||||
pub(super) fn build_match3d_background_generation_prompt(
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
) -> String {
|
||||
let style_clause = resolve_match3d_asset_style_prompt(config)
|
||||
.map(|style| format!("整体美术风格参考:{style}。"))
|
||||
.unwrap_or_default();
|
||||
@@ -596,7 +599,10 @@ pub(super) fn build_match3d_background_generation_prompt(config: &Match3DConfigJ
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_container_generation_prompt(config: &Match3DConfigJson, prompt: &str) -> String {
|
||||
pub(super) fn build_match3d_container_generation_prompt(
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
) -> String {
|
||||
let style_clause = resolve_match3d_asset_style_prompt(config)
|
||||
.map(|style| format!("整体美术风格参考:{style}。"))
|
||||
.unwrap_or_default();
|
||||
@@ -1183,7 +1189,9 @@ pub(super) async fn persist_match3d_generated_bytes(
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn require_match3d_oss_client(state: &AppState) -> Result<&platform_oss::OssClient, AppError> {
|
||||
pub(super) fn require_match3d_oss_client(
|
||||
state: &AppState,
|
||||
) -> Result<&platform_oss::OssClient, AppError> {
|
||||
state
|
||||
.oss_client()
|
||||
.ok_or_else(|| match3d_oss_config_error(&state.config))
|
||||
|
||||
@@ -7,7 +7,9 @@ use crate::{
|
||||
auth::require_bearer_auth,
|
||||
bark_battle::{
|
||||
create_bark_battle_draft, finish_bark_battle_run, get_bark_battle_run,
|
||||
get_bark_battle_runtime_config, publish_bark_battle_work, start_bark_battle_run,
|
||||
generate_bark_battle_image_asset, get_bark_battle_runtime_config,
|
||||
list_bark_battle_gallery, list_bark_battle_works, publish_bark_battle_work,
|
||||
start_bark_battle_run, update_bark_battle_draft_config,
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
@@ -21,6 +23,20 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/bark-battle/drafts/{draft_id}/config",
|
||||
post(update_bark_battle_draft_config).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/bark-battle/images/generate",
|
||||
post(generate_bark_battle_image_asset).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/creation/bark-battle/works/publish",
|
||||
post(publish_bark_battle_work).route_layer(middleware::from_fn_with_state(
|
||||
@@ -28,6 +44,17 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/works",
|
||||
get(list_bark_battle_works).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/gallery",
|
||||
get(list_bark_battle_gallery),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/bark-battle/works/{work_id}/config",
|
||||
get(get_bark_battle_runtime_config).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -199,11 +199,9 @@ fn cpu_usage_ratio_between_samples(
|
||||
|
||||
#[cfg(windows)]
|
||||
fn collect_process_metrics() -> Result<ProcessMetricsSnapshot, String> {
|
||||
use windows_sys::Win32::{
|
||||
System::{
|
||||
ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX},
|
||||
Threading::{GetCurrentProcess, GetCurrentProcessId, GetProcessHandleCount},
|
||||
},
|
||||
use windows_sys::Win32::System::{
|
||||
ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX},
|
||||
Threading::{GetCurrentProcess, GetCurrentProcessId, GetProcessHandleCount},
|
||||
};
|
||||
|
||||
let handle = unsafe { GetCurrentProcess() };
|
||||
@@ -212,11 +210,7 @@ fn collect_process_metrics() -> Result<ProcessMetricsSnapshot, String> {
|
||||
..Default::default()
|
||||
};
|
||||
let ok = unsafe {
|
||||
GetProcessMemoryInfo(
|
||||
handle,
|
||||
std::ptr::addr_of_mut!(counters).cast(),
|
||||
counters.cb,
|
||||
)
|
||||
GetProcessMemoryInfo(handle, std::ptr::addr_of_mut!(counters).cast(), counters.cb)
|
||||
};
|
||||
if ok == 0 {
|
||||
return Err("GetProcessMemoryInfo returned false".to_string());
|
||||
@@ -244,10 +238,7 @@ fn collect_process_metrics() -> Result<ProcessMetricsSnapshot, String> {
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_process_cpu_time_seconds(handle: windows_sys::Win32::Foundation::HANDLE) -> Option<f64> {
|
||||
use windows_sys::Win32::{
|
||||
Foundation::FILETIME,
|
||||
System::Threading::GetProcessTimes,
|
||||
};
|
||||
use windows_sys::Win32::{Foundation::FILETIME, System::Threading::GetProcessTimes};
|
||||
|
||||
let mut creation_time = FILETIME::default();
|
||||
let mut exit_time = FILETIME::default();
|
||||
@@ -337,8 +328,8 @@ fn collect_process_metrics() -> Result<ProcessMetricsSnapshot, String> {
|
||||
.ok_or_else(|| "missing VmSize/statm size field".to_string())?;
|
||||
let private_bytes = parse_status_kb(&status, "VmData:").map(|value| value * 1024);
|
||||
let cpu_time_seconds = linux_cpu_time_seconds(&stat)?;
|
||||
let thread_count = parse_status_u64(&status, "Threads:")
|
||||
.ok_or_else(|| "missing Threads field".to_string())?;
|
||||
let thread_count =
|
||||
parse_status_u64(&status, "Threads:").ok_or_else(|| "missing Threads field".to_string())?;
|
||||
|
||||
Ok(ProcessMetricsSnapshot {
|
||||
rss_bytes,
|
||||
@@ -427,11 +418,7 @@ fn parse_status_u64(status: &str, key: &str) -> Option<u64> {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_statm_pages(statm: &str, index: usize) -> Option<u64> {
|
||||
statm
|
||||
.split_whitespace()
|
||||
.nth(index)?
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
statm.split_whitespace().nth(index)?.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
|
||||
@@ -72,7 +72,7 @@ impl BarkBattleRuleset {
|
||||
standard_duration_ms: 30_000,
|
||||
min_duration_ms: 28_000,
|
||||
max_duration_ms: 35_000,
|
||||
min_bark_gap_ms: 250,
|
||||
min_bark_gap_ms: 150,
|
||||
trigger_count_tolerance: 2,
|
||||
min_volume: 0.0,
|
||||
max_volume: 1.0,
|
||||
|
||||
@@ -174,6 +174,7 @@ mod tests {
|
||||
#[test]
|
||||
fn flags_trigger_count_above_physical_limit_with_tolerance() {
|
||||
let ruleset = BarkBattleRuleset::v1();
|
||||
assert_eq!(ruleset.min_bark_gap_ms, 150);
|
||||
let mut input = metrics(30_000);
|
||||
input.trigger_count = input.duration_ms / ruleset.min_bark_gap_ms
|
||||
+ u64::from(ruleset.trigger_count_tolerance)
|
||||
|
||||
@@ -142,10 +142,10 @@ pub fn default_creation_entry_type_snapshots(
|
||||
"bark-battle",
|
||||
"汪汪声浪",
|
||||
"声控对战挑战",
|
||||
"敬请期待",
|
||||
"/creation-type-references/creative-agent.webp",
|
||||
"可创建",
|
||||
"/creation-type-references/bark-battle.webp",
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
85,
|
||||
updated_at_micros,
|
||||
),
|
||||
|
||||
@@ -246,9 +246,13 @@ mod tests {
|
||||
|
||||
assert_eq!(bark_battle.title, "汪汪声浪");
|
||||
assert!(bark_battle.visible);
|
||||
assert!(!bark_battle.open);
|
||||
assert_eq!(bark_battle.badge, "敬请期待");
|
||||
assert!(bark_battle.open);
|
||||
assert_eq!(bark_battle.badge, "可创建");
|
||||
assert_eq!(bark_battle.sort_order, 85);
|
||||
assert_eq!(
|
||||
bark_battle.image_src,
|
||||
"/creation-type-references/bark-battle.webp"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -522,8 +526,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn runtime_profile_beijing_day_key_uses_business_day_boundary() {
|
||||
let before_beijing_midnight = 1_714_927_999_999_999;
|
||||
let after_beijing_midnight = 1_714_928_000_000_000;
|
||||
// 中文注释:2024-05-06 00:00:00 Asia/Shanghai 前后 1 微秒。
|
||||
let before_beijing_midnight = 1_714_924_799_999_999;
|
||||
let after_beijing_midnight = 1_714_924_800_000_000;
|
||||
|
||||
assert_eq!(
|
||||
runtime_profile_beijing_day_key(before_beijing_midnight),
|
||||
|
||||
@@ -20,7 +20,7 @@ const OSS_V4_REQUEST: &str = "aliyun_v4_request";
|
||||
const OSS_V4_SERVICE: &str = "oss";
|
||||
const OSS_UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
|
||||
|
||||
pub const LEGACY_PUBLIC_PREFIXES: [&str; 10] = [
|
||||
pub const LEGACY_PUBLIC_PREFIXES: [&str; 11] = [
|
||||
"generated-character-drafts",
|
||||
"generated-characters",
|
||||
"generated-animations",
|
||||
@@ -30,6 +30,7 @@ pub const LEGACY_PUBLIC_PREFIXES: [&str; 10] = [
|
||||
"generated-puzzle-assets",
|
||||
"generated-custom-world-scenes",
|
||||
"generated-custom-world-covers",
|
||||
"generated-bark-battle-assets",
|
||||
"generated-qwen-sprites",
|
||||
];
|
||||
|
||||
@@ -51,6 +52,7 @@ pub enum LegacyAssetPrefix {
|
||||
PuzzleAssets,
|
||||
CustomWorldScenes,
|
||||
CustomWorldCovers,
|
||||
BarkBattleAssets,
|
||||
QwenSprites,
|
||||
}
|
||||
|
||||
@@ -238,6 +240,7 @@ impl LegacyAssetPrefix {
|
||||
"generated-puzzle-assets" => Some(Self::PuzzleAssets),
|
||||
"generated-custom-world-scenes" => Some(Self::CustomWorldScenes),
|
||||
"generated-custom-world-covers" => Some(Self::CustomWorldCovers),
|
||||
"generated-bark-battle-assets" => Some(Self::BarkBattleAssets),
|
||||
"generated-qwen-sprites" => Some(Self::QwenSprites),
|
||||
_ => None,
|
||||
}
|
||||
@@ -254,6 +257,7 @@ impl LegacyAssetPrefix {
|
||||
Self::PuzzleAssets => "generated-puzzle-assets",
|
||||
Self::CustomWorldScenes => "generated-custom-world-scenes",
|
||||
Self::CustomWorldCovers => "generated-custom-world-covers",
|
||||
Self::BarkBattleAssets => "generated-bark-battle-assets",
|
||||
Self::QwenSprites => "generated-qwen-sprites",
|
||||
}
|
||||
}
|
||||
@@ -1315,6 +1319,7 @@ mod tests {
|
||||
);
|
||||
assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-puzzle-assets"));
|
||||
assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-match3d-assets"));
|
||||
assert!(LEGACY_PUBLIC_PREFIXES.contains(&"generated-bark-battle-assets"));
|
||||
assert_eq!(LegacyAssetPrefix::parse("unknown"), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,14 @@ pub enum BarkBattleFinishStatus {
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum BarkBattleAssetSlot {
|
||||
PlayerCharacter,
|
||||
OpponentCharacter,
|
||||
UiBackground,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleReplacementConfig {
|
||||
@@ -39,8 +47,6 @@ pub struct BarkBattleReplacementConfig {
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -49,20 +55,19 @@ pub struct BarkBattleConfigEditorPayload {
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -71,20 +76,19 @@ pub struct BarkBattleDraftCreateRequest {
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
impl From<BarkBattleDraftCreateRequest> for BarkBattleConfigEditorPayload {
|
||||
@@ -92,15 +96,59 @@ impl From<BarkBattleDraftCreateRequest> for BarkBattleConfigEditorPayload {
|
||||
Self {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
theme_preset: value.theme_preset,
|
||||
player_dog_skin_preset: value.player_dog_skin_preset,
|
||||
opponent_dog_skin_preset: value.opponent_dog_skin_preset,
|
||||
theme_description: value.theme_description,
|
||||
player_image_description: value.player_image_description,
|
||||
opponent_image_description: value.opponent_image_description,
|
||||
onomatopoeia: value.onomatopoeia,
|
||||
player_character_image_src: value.player_character_image_src,
|
||||
opponent_character_image_src: value.opponent_character_image_src,
|
||||
ui_background_image_src: value.ui_background_image_src,
|
||||
difficulty_preset: value.difficulty_preset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleDraftConfigUpdateRequest {
|
||||
pub draft_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub work_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_version: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ruleset_version: Option<String>,
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
}
|
||||
|
||||
impl From<BarkBattleDraftConfigUpdateRequest> for BarkBattleConfigEditorPayload {
|
||||
fn from(value: BarkBattleDraftConfigUpdateRequest) -> Self {
|
||||
Self {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
theme_description: value.theme_description,
|
||||
player_image_description: value.player_image_description,
|
||||
opponent_image_description: value.opponent_image_description,
|
||||
onomatopoeia: value.onomatopoeia,
|
||||
player_character_image_src: value.player_character_image_src,
|
||||
opponent_character_image_src: value.opponent_character_image_src,
|
||||
ui_background_image_src: value.ui_background_image_src,
|
||||
bark_sound_src: value.bark_sound_src,
|
||||
difficulty_preset: value.difficulty_preset,
|
||||
leaderboard_enabled: value.leaderboard_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +163,30 @@ pub struct BarkBattleWorkPublishRequest {
|
||||
pub published_snapshot: Option<BarkBattleConfigEditorPayload>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleImageAssetGenerateRequest {
|
||||
pub slot: BarkBattleAssetSlot,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub draft_id: Option<String>,
|
||||
pub config: BarkBattleConfigEditorPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleGeneratedImageAsset {
|
||||
pub image_src: String,
|
||||
pub asset_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub source_type: Option<String>,
|
||||
pub model: String,
|
||||
pub size: String,
|
||||
pub task_id: String,
|
||||
pub prompt: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub actual_prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleDraftConfig {
|
||||
@@ -128,20 +200,19 @@ pub struct BarkBattleDraftConfig {
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
#[serde(default)]
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
@@ -154,15 +225,14 @@ impl Default for BarkBattleDraftConfig {
|
||||
ruleset_version: None,
|
||||
title: String::new(),
|
||||
description: None,
|
||||
theme_preset: String::new(),
|
||||
player_dog_skin_preset: String::new(),
|
||||
opponent_dog_skin_preset: String::new(),
|
||||
theme_description: String::new(),
|
||||
player_image_description: String::new(),
|
||||
opponent_image_description: String::new(),
|
||||
onomatopoeia: None,
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
leaderboard_enabled: true,
|
||||
updated_at: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -180,19 +250,18 @@ pub struct BarkBattlePublishedConfig {
|
||||
pub title: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub updated_at: String,
|
||||
pub published_at: String,
|
||||
}
|
||||
@@ -210,21 +279,75 @@ pub struct BarkBattleRuntimeConfig {
|
||||
pub draw_threshold: f32,
|
||||
pub min_bark_gap_ms: u64,
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleWorkSummary {
|
||||
pub work_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub draft_id: Option<String>,
|
||||
pub owner_user_id: String,
|
||||
pub author_display_name: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub onomatopoeia: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
pub difficulty_preset: BarkBattleDifficultyPreset,
|
||||
pub status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub generation_status: Option<String>,
|
||||
pub publish_ready: bool,
|
||||
pub play_count: u64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub finish_count: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub win_count: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub draw_count: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub loss_count: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub recent_play_count_7d: Option<u64>,
|
||||
pub updated_at: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleWorksResponse {
|
||||
#[serde(default)]
|
||||
pub items: Vec<BarkBattleWorkSummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleWorkDetailResponse {
|
||||
pub item: BarkBattleWorkSummary,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleRunStartRequest {
|
||||
@@ -425,6 +548,115 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn editor_and_runtime_contract_use_description_fields_only() {
|
||||
let editor = BarkBattleConfigEditorPayload {
|
||||
title: "周末狗狗杯".to_string(),
|
||||
description: Some("轻配置草稿".to_string()),
|
||||
theme_description: "霓虹公园里的欢乐擂台".to_string(),
|
||||
player_image_description: "戴红围巾的柴犬主角".to_string(),
|
||||
opponent_image_description: "蓝色护目镜哈士奇对手".to_string(),
|
||||
onomatopoeia: Some(vec!["轰汪!".to_string(), "炸场!".to_string()]),
|
||||
player_character_image_src: Some("/generated-bark-battle/player.png".to_string()),
|
||||
opponent_character_image_src: Some("https://example.test/opponent.png".to_string()),
|
||||
ui_background_image_src: Some("/generated-bark-battle/ui.png".to_string()),
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Hard,
|
||||
};
|
||||
let payload = serde_json::to_value(editor).expect("config should serialize");
|
||||
|
||||
assert_eq!(payload["themeDescription"], json!("霓虹公园里的欢乐擂台"));
|
||||
assert_eq!(
|
||||
payload["playerImageDescription"],
|
||||
json!("戴红围巾的柴犬主角")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["opponentImageDescription"],
|
||||
json!("蓝色护目镜哈士奇对手")
|
||||
);
|
||||
assert_eq!(payload["onomatopoeia"], json!(["轰汪!", "炸场!"]));
|
||||
for removed in [
|
||||
"themePreset",
|
||||
"playerDogSkinPreset",
|
||||
"opponentDogSkinPreset",
|
||||
"barkSoundSrc",
|
||||
"leaderboardEnabled",
|
||||
] {
|
||||
assert!(
|
||||
!payload.as_object().unwrap().contains_key(removed),
|
||||
"{removed} must not remain in v1 public config payload"
|
||||
);
|
||||
}
|
||||
|
||||
let runtime = BarkBattleRuntimeConfig {
|
||||
work_id: "bark-battle-work-1".to_string(),
|
||||
config_version: 1,
|
||||
ruleset_version: "bark-battle-ruleset-v1".to_string(),
|
||||
play_type_id: "bark-battle".to_string(),
|
||||
duration_ms: 30_000,
|
||||
energy_min: 0.0,
|
||||
energy_max: 100.0,
|
||||
draw_threshold: 5.0,
|
||||
min_bark_gap_ms: 220,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
theme_description: "阳光草坪".to_string(),
|
||||
player_image_description: "小柴犬".to_string(),
|
||||
opponent_image_description: "大金毛".to_string(),
|
||||
onomatopoeia: Some(vec!["轰汪!".to_string(), "燃起来!".to_string()]),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
updated_at: "2026-05-20T00:00:00Z".to_string(),
|
||||
};
|
||||
let payload = serde_json::to_value(runtime).expect("runtime should serialize");
|
||||
assert_eq!(payload["themeDescription"], json!("阳光草坪"));
|
||||
assert!(
|
||||
!payload
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.contains_key("leaderboardEnabled")
|
||||
);
|
||||
assert!(!payload.as_object().unwrap().contains_key("barkSoundSrc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn work_summary_responses_use_public_gallery_contract() {
|
||||
let response = BarkBattleWorksResponse {
|
||||
items: vec![BarkBattleWorkSummary {
|
||||
work_id: "bark-battle-work-1".to_string(),
|
||||
draft_id: Some("bark-battle-draft-1".to_string()),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
author_display_name: "玩家".to_string(),
|
||||
title: "汪汪测试杯".to_string(),
|
||||
summary: "轻量公开卡片".to_string(),
|
||||
theme_description: "阳光草坪".to_string(),
|
||||
player_image_description: "小柴犬".to_string(),
|
||||
opponent_image_description: "大金毛".to_string(),
|
||||
onomatopoeia: Some(vec!["轰汪!".to_string(), "燃起来!".to_string()]),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
status: "published".to_string(),
|
||||
generation_status: Some("ready".to_string()),
|
||||
publish_ready: true,
|
||||
play_count: 3,
|
||||
finish_count: Some(2),
|
||||
win_count: Some(1),
|
||||
draw_count: Some(1),
|
||||
loss_count: Some(0),
|
||||
recent_play_count_7d: Some(2),
|
||||
updated_at: "2026-05-20T00:00:00Z".to_string(),
|
||||
published_at: Some("2026-05-20T00:00:00Z".to_string()),
|
||||
}],
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(response).expect("works response should serialize");
|
||||
|
||||
assert_eq!(payload["items"][0]["themeDescription"], json!("阳光草坪"));
|
||||
assert_eq!(payload["items"][0]["recentPlayCount7d"], json!(2));
|
||||
assert_eq!(payload["items"][0]["status"], json!("published"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_config_defaults_to_normal_difficulty() {
|
||||
let config = BarkBattleDraftConfig::default();
|
||||
@@ -523,15 +755,14 @@ mod tests {
|
||||
ruleset_version: Some("bark-battle-ruleset-v1".to_string()),
|
||||
title: "汪汪测试杯".to_string(),
|
||||
description: None,
|
||||
theme_preset: "sunny-yard".to_string(),
|
||||
player_dog_skin_preset: "主角".to_string(),
|
||||
opponent_dog_skin_preset: "对手".to_string(),
|
||||
theme_description: "阳光草坪".to_string(),
|
||||
player_image_description: "主角".to_string(),
|
||||
opponent_image_description: "对手".to_string(),
|
||||
onomatopoeia: None,
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
leaderboard_enabled: true,
|
||||
updated_at: "2026-05-14T10:00:00.000Z".to_string(),
|
||||
};
|
||||
|
||||
@@ -540,10 +771,96 @@ mod tests {
|
||||
assert_eq!(payload["draftId"], json!("bark-battle-draft-1"));
|
||||
assert_eq!(payload["workId"], json!("bark-battle-work-1"));
|
||||
assert_eq!(payload["configVersion"], json!(2));
|
||||
assert_eq!(payload["rulesetVersion"], json!("bark-battle-ruleset-v1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_config_update_request_serializes_generated_assets() {
|
||||
let update = BarkBattleDraftConfigUpdateRequest {
|
||||
draft_id: "bark-battle-draft-1".to_string(),
|
||||
work_id: Some("BB-12345678".to_string()),
|
||||
config_version: Some(2),
|
||||
ruleset_version: Some("bark-battle-ruleset-v1".to_string()),
|
||||
title: "汪汪测试杯".to_string(),
|
||||
description: None,
|
||||
theme_description: "阳光草坪".to_string(),
|
||||
player_image_description: "主角".to_string(),
|
||||
opponent_image_description: "对手".to_string(),
|
||||
onomatopoeia: Some(vec!["炸场!".to_string(), "破阵!".to_string()]),
|
||||
player_character_image_src: Some("/generated-bark-battle/player.png".to_string()),
|
||||
opponent_character_image_src: Some("/generated-bark-battle/opponent.png".to_string()),
|
||||
ui_background_image_src: Some("/generated-bark-battle/background.png".to_string()),
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(update).expect("draft update should serialize");
|
||||
|
||||
assert_eq!(payload["draftId"], json!("bark-battle-draft-1"));
|
||||
assert_eq!(payload["workId"], json!("BB-12345678"));
|
||||
assert_eq!(
|
||||
payload["rulesetVersion"],
|
||||
json!("bark-battle-ruleset-v1")
|
||||
payload["playerCharacterImageSrc"],
|
||||
json!("/generated-bark-battle/player.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["opponentCharacterImageSrc"],
|
||||
json!("/generated-bark-battle/opponent.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["uiBackgroundImageSrc"],
|
||||
json!("/generated-bark-battle/background.png")
|
||||
);
|
||||
assert!(!payload.as_object().unwrap().contains_key("barkSoundSrc"));
|
||||
assert!(
|
||||
!payload
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.contains_key("leaderboardEnabled")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_generation_request_uses_dedicated_asset_slot_and_result_prompt() {
|
||||
let request = BarkBattleImageAssetGenerateRequest {
|
||||
slot: BarkBattleAssetSlot::OpponentCharacter,
|
||||
draft_id: Some("bark-battle-draft-1".to_string()),
|
||||
config: BarkBattleConfigEditorPayload {
|
||||
title: "汪汪冠军杯".to_string(),
|
||||
description: Some(String::new()),
|
||||
theme_description: "霓虹公园擂台".to_string(),
|
||||
player_image_description: "红围巾柴犬".to_string(),
|
||||
opponent_image_description: "蓝头带哈士奇".to_string(),
|
||||
onomatopoeia: Some(vec!["轰汪!".to_string(), "炸场!".to_string()]),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Normal,
|
||||
},
|
||||
};
|
||||
let payload = serde_json::to_value(request).expect("request should serialize");
|
||||
|
||||
assert_eq!(payload["slot"], json!("opponent-character"));
|
||||
assert_eq!(
|
||||
payload["config"]["opponentImageDescription"],
|
||||
json!("蓝头带哈士奇")
|
||||
);
|
||||
|
||||
let response = BarkBattleGeneratedImageAsset {
|
||||
image_src: "/generated-bark-battle-assets/draft/opponent/image.webp".to_string(),
|
||||
asset_id: "asset-1".to_string(),
|
||||
source_type: Some("generated".to_string()),
|
||||
model: "gpt-image-2".to_string(),
|
||||
size: "1024*1024".to_string(),
|
||||
task_id: "task-1".to_string(),
|
||||
prompt: "后端拼装后的对手形象 prompt".to_string(),
|
||||
actual_prompt: None,
|
||||
};
|
||||
let payload = serde_json::to_value(response).expect("response should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload["imageSrc"],
|
||||
json!("/generated-bark-battle-assets/draft/opponent/image.webp")
|
||||
);
|
||||
assert_eq!(payload["prompt"], json!("后端拼装后的对手形象 prompt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -551,15 +868,14 @@ mod tests {
|
||||
let config = BarkBattleConfigEditorPayload {
|
||||
title: "周末狗狗杯".to_string(),
|
||||
description: Some("轻配置草稿".to_string()),
|
||||
theme_preset: "neon-park".to_string(),
|
||||
player_dog_skin_preset: "shiba".to_string(),
|
||||
opponent_dog_skin_preset: "husky".to_string(),
|
||||
theme_description: "霓虹公园".to_string(),
|
||||
player_image_description: "柴犬主角".to_string(),
|
||||
opponent_image_description: "哈士奇对手".to_string(),
|
||||
onomatopoeia: Some(vec!["轰汪!".to_string(), "冲啊!".to_string()]),
|
||||
player_character_image_src: Some("/generated-bark-battle/player.png".to_string()),
|
||||
opponent_character_image_src: Some("https://example.test/opponent.png".to_string()),
|
||||
ui_background_image_src: Some("/generated-bark-battle/ui.png".to_string()),
|
||||
bark_sound_src: Some("/generated-bark-battle/bark.mp3".to_string()),
|
||||
difficulty_preset: BarkBattleDifficultyPreset::Hard,
|
||||
leaderboard_enabled: true,
|
||||
};
|
||||
|
||||
let payload = serde_json::to_value(config).expect("config should serialize");
|
||||
@@ -576,10 +892,7 @@ mod tests {
|
||||
payload["uiBackgroundImageSrc"],
|
||||
json!("/generated-bark-battle/ui.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["barkSoundSrc"],
|
||||
json!("/generated-bark-battle/bark.mp3")
|
||||
);
|
||||
assert!(!payload.as_object().unwrap().contains_key("barkSoundSrc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type BarkBattleDraftCreateRecordInput = BarkBattleDraftCreateInput;
|
||||
pub type BarkBattleDraftConfigUpsertRecordInput = BarkBattleDraftConfigUpsertInput;
|
||||
@@ -44,6 +45,32 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_bark_battle_draft_config(
|
||||
&self,
|
||||
draft_id: String,
|
||||
owner_user_id: String,
|
||||
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
|
||||
self.read_after_connect("get_bark_battle_draft_config", move |connection| {
|
||||
let row = connection
|
||||
.db()
|
||||
.bark_battle_draft_config()
|
||||
.draft_id()
|
||||
.find(&draft_id)
|
||||
.ok_or_else(|| {
|
||||
SpacetimeClientError::procedure_failed(Some(
|
||||
"bark_battle draft 不存在".to_string(),
|
||||
))
|
||||
})?;
|
||||
if row.owner_user_id != owner_user_id {
|
||||
return Err(SpacetimeClientError::procedure_failed(Some(
|
||||
"bark_battle draft owner 不匹配".to_string(),
|
||||
)));
|
||||
}
|
||||
Ok(map_bark_battle_draft_config_row(row))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn publish_bark_battle_work(
|
||||
&self,
|
||||
input: BarkBattleWorkPublishRecordInput,
|
||||
@@ -142,4 +169,83 @@ impl SpacetimeClient {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_bark_battle_works(
|
||||
&self,
|
||||
owner_user_id: String,
|
||||
) -> Result<Vec<serde_json::Value>, SpacetimeClientError> {
|
||||
self.read_after_connect("list_bark_battle_works", move |connection| {
|
||||
let owner_user_id = owner_user_id.as_str();
|
||||
let drafts: Vec<serde_json::Value> = connection
|
||||
.db()
|
||||
.bark_battle_draft_config()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == owner_user_id)
|
||||
.map(map_bark_battle_draft_config_row)
|
||||
.collect();
|
||||
let published: Vec<serde_json::Value> = connection
|
||||
.db()
|
||||
.bark_battle_published_config()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == owner_user_id)
|
||||
.map(map_bark_battle_published_config_row)
|
||||
.collect();
|
||||
|
||||
let mut works_by_id: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
for work in published.into_iter().chain(drafts) {
|
||||
let Some(work_id) = work
|
||||
.get("workId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToString::to_string)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
works_by_id.entry(work_id).or_insert(work);
|
||||
}
|
||||
|
||||
let mut works: Vec<serde_json::Value> = works_by_id.into_values().collect();
|
||||
works.sort_by(|left: &serde_json::Value, right: &serde_json::Value| {
|
||||
let left_updated_at = left
|
||||
.get("updatedAtMicros")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or_default();
|
||||
let right_updated_at = right
|
||||
.get("updatedAtMicros")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or_default();
|
||||
right_updated_at.cmp(&left_updated_at)
|
||||
});
|
||||
Ok(works)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_bark_battle_gallery(
|
||||
&self,
|
||||
) -> Result<Vec<serde_json::Value>, SpacetimeClientError> {
|
||||
self.read_after_connect("list_bark_battle_gallery", move |connection| {
|
||||
let recent_play_counts = public_work_recent_play_counts(connection, "bark-battle");
|
||||
let mut items = connection
|
||||
.db()
|
||||
.bark_battle_gallery_view()
|
||||
.iter()
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.work_id.cmp(&right.work_id))
|
||||
});
|
||||
Ok(items
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let recent_play_count_7d =
|
||||
recent_play_counts.get(&item.work_id).copied().unwrap_or(0);
|
||||
map_bark_battle_gallery_view_row(item, recent_play_count_7d)
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +550,7 @@ impl SpacetimeClient {
|
||||
) -> Result<Vec<SubscriptionHandle>, SpacetimeClientError> {
|
||||
let mut subscriptions = Vec::new();
|
||||
for query in [
|
||||
"SELECT * FROM bark_battle_gallery_view",
|
||||
"SELECT * FROM puzzle_gallery_card_view",
|
||||
"SELECT * FROM custom_world_gallery_entry",
|
||||
"SELECT * FROM match_3_d_gallery_view",
|
||||
@@ -570,6 +571,7 @@ impl SpacetimeClient {
|
||||
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'",
|
||||
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'visual-novel'",
|
||||
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'big-fish'",
|
||||
"SELECT * FROM public_work_play_daily_stat WHERE source_type = 'bark-battle'",
|
||||
"SELECT * FROM creation_entry_config",
|
||||
"SELECT * FROM creation_entry_type_config",
|
||||
] {
|
||||
|
||||
@@ -112,8 +112,9 @@ pub(crate) use self::auth::{
|
||||
map_auth_store_snapshot_import_procedure_result, map_auth_store_snapshot_procedure_result,
|
||||
};
|
||||
pub(crate) use self::bark_battle::{
|
||||
map_bark_battle_draft_config_procedure_result, map_bark_battle_run_procedure_result,
|
||||
map_bark_battle_runtime_config_procedure_result,
|
||||
map_bark_battle_draft_config_procedure_result, map_bark_battle_draft_config_row,
|
||||
map_bark_battle_gallery_view_row, map_bark_battle_published_config_row,
|
||||
map_bark_battle_run_procedure_result, map_bark_battle_runtime_config_procedure_result,
|
||||
};
|
||||
pub(crate) use self::big_fish::{
|
||||
map_big_fish_gallery_view_row, map_big_fish_run_procedure_result,
|
||||
|
||||
@@ -36,6 +36,70 @@ pub(crate) fn map_bark_battle_run_procedure_result(
|
||||
.map(bark_battle_run_to_value)
|
||||
}
|
||||
|
||||
pub(crate) fn map_bark_battle_draft_config_row(
|
||||
row: BarkBattleDraftConfigRow,
|
||||
) -> BarkBattleDraftConfigRecord {
|
||||
serde_json::json!({
|
||||
"draftId": row.draft_id,
|
||||
"ownerUserId": row.owner_user_id,
|
||||
"workId": row.work_id,
|
||||
"configVersion": row.config_version,
|
||||
"rulesetVersion": row.ruleset_version,
|
||||
"difficultyPreset": row.difficulty_preset,
|
||||
"configJson": row.config_json,
|
||||
"editorStateJson": row.editor_state_json,
|
||||
"createdAtMicros": row.created_at.to_micros_since_unix_epoch(),
|
||||
"updatedAtMicros": row.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_bark_battle_published_config_row(
|
||||
row: BarkBattlePublishedConfigRow,
|
||||
) -> BarkBattleRuntimeConfigRecord {
|
||||
serde_json::json!({
|
||||
"workId": row.work_id,
|
||||
"ownerUserId": row.owner_user_id,
|
||||
"sourceDraftId": row.source_draft_id,
|
||||
"configVersion": row.config_version,
|
||||
"rulesetVersion": row.ruleset_version,
|
||||
"difficultyPreset": row.difficulty_preset,
|
||||
"configJson": row.config_json,
|
||||
"publishedSnapshotJson": row.published_snapshot_json,
|
||||
"publishedAtMicros": row.published_at.to_micros_since_unix_epoch(),
|
||||
"updatedAtMicros": row.updated_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_bark_battle_gallery_view_row(
|
||||
row: BarkBattleGalleryViewRow,
|
||||
recent_play_count_7d: u32,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"workId": row.work_id,
|
||||
"ownerUserId": row.owner_user_id,
|
||||
"sourceDraftId": row.source_draft_id,
|
||||
"configVersion": row.config_version,
|
||||
"rulesetVersion": row.ruleset_version,
|
||||
"difficultyPreset": row.difficulty_preset,
|
||||
"title": row.title,
|
||||
"description": row.description,
|
||||
"themeDescription": row.theme_description,
|
||||
"playerImageDescription": row.player_image_description,
|
||||
"opponentImageDescription": row.opponent_image_description,
|
||||
"onomatopoeia": row.onomatopoeia,
|
||||
"playerCharacterImageSrc": row.player_character_image_src,
|
||||
"opponentCharacterImageSrc": row.opponent_character_image_src,
|
||||
"uiBackgroundImageSrc": row.ui_background_image_src,
|
||||
"status": "published",
|
||||
"publishReady": true,
|
||||
"playCount": row.play_count,
|
||||
"finishCount": row.finish_count,
|
||||
"recentPlayCount7d": recent_play_count_7d,
|
||||
"updatedAtMicros": row.updated_at_micros,
|
||||
"publishedAtMicros": row.published_at_micros,
|
||||
})
|
||||
}
|
||||
|
||||
fn bark_battle_draft_config_to_value(snapshot: BarkBattleDraftConfigSnapshot) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"draftId": snapshot.draft_id,
|
||||
@@ -44,7 +108,6 @@ fn bark_battle_draft_config_to_value(snapshot: BarkBattleDraftConfigSnapshot) ->
|
||||
"configVersion": snapshot.config_version,
|
||||
"rulesetVersion": snapshot.ruleset_version,
|
||||
"difficultyPreset": snapshot.difficulty_preset,
|
||||
"leaderboardEnabled": snapshot.leaderboard_enabled,
|
||||
"configJson": snapshot.config_json,
|
||||
"editorStateJson": snapshot.editor_state_json,
|
||||
"createdAtMicros": snapshot.created_at_micros,
|
||||
@@ -62,7 +125,6 @@ fn bark_battle_runtime_config_to_value(
|
||||
"configVersion": snapshot.config_version,
|
||||
"rulesetVersion": snapshot.ruleset_version,
|
||||
"difficultyPreset": snapshot.difficulty_preset,
|
||||
"leaderboardEnabled": snapshot.leaderboard_enabled,
|
||||
"configJson": snapshot.config_json,
|
||||
"publishedSnapshotJson": snapshot.published_snapshot_json,
|
||||
"publishedAtMicros": snapshot.published_at_micros,
|
||||
@@ -78,7 +140,6 @@ fn bark_battle_run_to_value(snapshot: BarkBattleRunSnapshot) -> serde_json::Valu
|
||||
"configVersion": snapshot.config_version,
|
||||
"rulesetVersion": snapshot.ruleset_version,
|
||||
"difficultyPreset": snapshot.difficulty_preset,
|
||||
"leaderboardEnabled": snapshot.leaderboard_enabled,
|
||||
"status": snapshot.status,
|
||||
"clientStartedAtMicros": snapshot.client_started_at_micros,
|
||||
"serverStartedAtMicros": snapshot.server_started_at_micros,
|
||||
@@ -92,3 +153,38 @@ fn bark_battle_run_to_value(snapshot: BarkBattleRunSnapshot) -> serde_json::Valu
|
||||
"scoreId": snapshot.score_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bark_battle_gallery_mapper_keeps_custom_onomatopoeia() {
|
||||
let row = BarkBattleGalleryViewRow {
|
||||
work_id: "BB-33333333".to_string(),
|
||||
owner_user_id: "user-3".to_string(),
|
||||
source_draft_id: Some("bark-battle-draft-3".to_string()),
|
||||
config_version: 1,
|
||||
ruleset_version: "bark-battle-ruleset-v1".to_string(),
|
||||
difficulty_preset: "normal".to_string(),
|
||||
title: "声浪公开赛".to_string(),
|
||||
description: "画廊映射测试".to_string(),
|
||||
theme_description: "霓虹竞技场".to_string(),
|
||||
player_image_description: "星际猫骑士".to_string(),
|
||||
opponent_image_description: "机器人拳手".to_string(),
|
||||
onomatopoeia: vec!["轰!".to_string(), "炸场!".to_string()],
|
||||
player_character_image_src: Some("/assets/player.png".to_string()),
|
||||
opponent_character_image_src: Some("/assets/opponent.png".to_string()),
|
||||
ui_background_image_src: Some("/assets/background.png".to_string()),
|
||||
play_count: 8,
|
||||
finish_count: 5,
|
||||
updated_at_micros: 1_713_686_401_234_567,
|
||||
published_at_micros: 1_713_686_401_234_000,
|
||||
};
|
||||
|
||||
let value = map_bark_battle_gallery_view_row(row, 3);
|
||||
|
||||
assert_eq!(value["onomatopoeia"], serde_json::json!(["轰!", "炸场!"]));
|
||||
assert_eq!(value["recentPlayCount7d"], serde_json::json!(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ pub mod bark_battle_draft_config_snapshot_type;
|
||||
pub mod bark_battle_draft_config_table;
|
||||
pub mod bark_battle_draft_config_upsert_input_type;
|
||||
pub mod bark_battle_draft_create_input_type;
|
||||
pub mod bark_battle_gallery_view_row_type;
|
||||
pub mod bark_battle_gallery_view_table;
|
||||
pub mod bark_battle_leaderboard_entry_row_type;
|
||||
pub mod bark_battle_leaderboard_entry_table;
|
||||
pub mod bark_battle_personal_best_projection_row_type;
|
||||
@@ -1025,6 +1027,8 @@ pub use bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot;
|
||||
pub use bark_battle_draft_config_table::*;
|
||||
pub use bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput;
|
||||
pub use bark_battle_draft_create_input_type::BarkBattleDraftCreateInput;
|
||||
pub use bark_battle_gallery_view_row_type::BarkBattleGalleryViewRow;
|
||||
pub use bark_battle_gallery_view_table::*;
|
||||
pub use bark_battle_leaderboard_entry_row_type::BarkBattleLeaderboardEntryRow;
|
||||
pub use bark_battle_leaderboard_entry_table::*;
|
||||
pub use bark_battle_personal_best_projection_row_type::BarkBattlePersonalBestProjectionRow;
|
||||
@@ -2143,6 +2147,7 @@ pub struct DbUpdate {
|
||||
auth_store_projection_meta: __sdk::TableUpdate<AuthStoreProjectionMeta>,
|
||||
auth_store_snapshot: __sdk::TableUpdate<AuthStoreSnapshot>,
|
||||
bark_battle_draft_config: __sdk::TableUpdate<BarkBattleDraftConfigRow>,
|
||||
bark_battle_gallery_view: __sdk::TableUpdate<BarkBattleGalleryViewRow>,
|
||||
bark_battle_leaderboard_entry: __sdk::TableUpdate<BarkBattleLeaderboardEntryRow>,
|
||||
bark_battle_personal_best_projection: __sdk::TableUpdate<BarkBattlePersonalBestProjectionRow>,
|
||||
bark_battle_published_config: __sdk::TableUpdate<BarkBattlePublishedConfigRow>,
|
||||
@@ -2272,6 +2277,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
||||
"bark_battle_draft_config" => db_update.bark_battle_draft_config.append(
|
||||
bark_battle_draft_config_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"bark_battle_gallery_view" => db_update.bark_battle_gallery_view.append(
|
||||
bark_battle_gallery_view_table::parse_table_update(table_update)?,
|
||||
),
|
||||
"bark_battle_leaderboard_entry" => db_update.bark_battle_leaderboard_entry.append(
|
||||
bark_battle_leaderboard_entry_table::parse_table_update(table_update)?,
|
||||
),
|
||||
@@ -3008,6 +3016,10 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
&self.visual_novel_work_profile,
|
||||
)
|
||||
.with_updates_by_pk(|row| &row.profile_id);
|
||||
diff.bark_battle_gallery_view = cache.apply_diff_to_table::<BarkBattleGalleryViewRow>(
|
||||
"bark_battle_gallery_view",
|
||||
&self.bark_battle_gallery_view,
|
||||
);
|
||||
diff.big_fish_gallery_view = cache.apply_diff_to_table::<BigFishWorkSummarySnapshot>(
|
||||
"big_fish_gallery_view",
|
||||
&self.big_fish_gallery_view,
|
||||
@@ -3078,6 +3090,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"bark_battle_draft_config" => db_update
|
||||
.bark_battle_draft_config
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"bark_battle_gallery_view" => db_update
|
||||
.bark_battle_gallery_view
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
"bark_battle_leaderboard_entry" => db_update
|
||||
.bark_battle_leaderboard_entry
|
||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||
@@ -3376,6 +3391,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
||||
"bark_battle_draft_config" => db_update
|
||||
.bark_battle_draft_config
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"bark_battle_gallery_view" => db_update
|
||||
.bark_battle_gallery_view
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
"bark_battle_leaderboard_entry" => db_update
|
||||
.bark_battle_leaderboard_entry
|
||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||
@@ -3650,6 +3668,7 @@ pub struct AppliedDiff<'r> {
|
||||
auth_store_projection_meta: __sdk::TableAppliedDiff<'r, AuthStoreProjectionMeta>,
|
||||
auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>,
|
||||
bark_battle_draft_config: __sdk::TableAppliedDiff<'r, BarkBattleDraftConfigRow>,
|
||||
bark_battle_gallery_view: __sdk::TableAppliedDiff<'r, BarkBattleGalleryViewRow>,
|
||||
bark_battle_leaderboard_entry: __sdk::TableAppliedDiff<'r, BarkBattleLeaderboardEntryRow>,
|
||||
bark_battle_personal_best_projection:
|
||||
__sdk::TableAppliedDiff<'r, BarkBattlePersonalBestProjectionRow>,
|
||||
@@ -3805,6 +3824,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
||||
&self.bark_battle_draft_config,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<BarkBattleGalleryViewRow>(
|
||||
"bark_battle_gallery_view",
|
||||
&self.bark_battle_gallery_view,
|
||||
event,
|
||||
);
|
||||
callbacks.invoke_table_row_callbacks::<BarkBattleLeaderboardEntryRow>(
|
||||
"bark_battle_leaderboard_entry",
|
||||
&self.bark_battle_leaderboard_entry,
|
||||
@@ -4876,6 +4900,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
auth_store_projection_meta_table::register_table(client_cache);
|
||||
auth_store_snapshot_table::register_table(client_cache);
|
||||
bark_battle_draft_config_table::register_table(client_cache);
|
||||
bark_battle_gallery_view_table::register_table(client_cache);
|
||||
bark_battle_leaderboard_entry_table::register_table(client_cache);
|
||||
bark_battle_personal_best_projection_table::register_table(client_cache);
|
||||
bark_battle_published_config_table::register_table(client_cache);
|
||||
@@ -4973,6 +4998,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
||||
"auth_store_projection_meta",
|
||||
"auth_store_snapshot",
|
||||
"bark_battle_draft_config",
|
||||
"bark_battle_gallery_view",
|
||||
"bark_battle_leaderboard_entry",
|
||||
"bark_battle_personal_best_projection",
|
||||
"bark_battle_published_config",
|
||||
|
||||
@@ -13,7 +13,6 @@ pub struct BarkBattleDraftConfigSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub editor_state_json: String,
|
||||
pub created_at_micros: i64,
|
||||
|
||||
@@ -13,7 +13,6 @@ pub struct BarkBattleDraftConfigUpsertInput {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ pub struct BarkBattleDraftCreateInput {
|
||||
pub work_id: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
pub difficulty_preset: Option<String>,
|
||||
pub leaderboard_enabled: Option<bool>,
|
||||
pub editor_state_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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 BarkBattleGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_draft_id: Option<String>,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
pub onomatopoeia: Vec<String>,
|
||||
pub player_character_image_src: Option<String>,
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
pub ui_background_image_src: Option<String>,
|
||||
pub play_count: u64,
|
||||
pub finish_count: u64,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for BarkBattleGalleryViewRow {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
/// Column accessor struct for the table `BarkBattleGalleryViewRow`.
|
||||
///
|
||||
/// Provides typed access to columns for query building.
|
||||
pub struct BarkBattleGalleryViewRowCols {
|
||||
pub work_id: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub source_draft_id: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, Option<String>>,
|
||||
pub config_version: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, u64>,
|
||||
pub ruleset_version: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub difficulty_preset: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub title: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub description: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub theme_description: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub player_image_description: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub opponent_image_description: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, String>,
|
||||
pub onomatopoeia: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, Vec<String>>,
|
||||
pub player_character_image_src:
|
||||
__sdk::__query_builder::Col<BarkBattleGalleryViewRow, Option<String>>,
|
||||
pub opponent_character_image_src:
|
||||
__sdk::__query_builder::Col<BarkBattleGalleryViewRow, Option<String>>,
|
||||
pub ui_background_image_src:
|
||||
__sdk::__query_builder::Col<BarkBattleGalleryViewRow, Option<String>>,
|
||||
pub play_count: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, u64>,
|
||||
pub finish_count: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, u64>,
|
||||
pub updated_at_micros: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, i64>,
|
||||
pub published_at_micros: __sdk::__query_builder::Col<BarkBattleGalleryViewRow, i64>,
|
||||
}
|
||||
|
||||
impl __sdk::__query_builder::HasCols for BarkBattleGalleryViewRow {
|
||||
type Cols = BarkBattleGalleryViewRowCols;
|
||||
fn cols(table_name: &'static str) -> Self::Cols {
|
||||
BarkBattleGalleryViewRowCols {
|
||||
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
|
||||
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
|
||||
source_draft_id: __sdk::__query_builder::Col::new(table_name, "source_draft_id"),
|
||||
config_version: __sdk::__query_builder::Col::new(table_name, "config_version"),
|
||||
ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"),
|
||||
difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"),
|
||||
title: __sdk::__query_builder::Col::new(table_name, "title"),
|
||||
description: __sdk::__query_builder::Col::new(table_name, "description"),
|
||||
theme_description: __sdk::__query_builder::Col::new(table_name, "theme_description"),
|
||||
player_image_description: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"player_image_description",
|
||||
),
|
||||
opponent_image_description: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"opponent_image_description",
|
||||
),
|
||||
onomatopoeia: __sdk::__query_builder::Col::new(table_name, "onomatopoeia"),
|
||||
player_character_image_src: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"player_character_image_src",
|
||||
),
|
||||
opponent_character_image_src: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"opponent_character_image_src",
|
||||
),
|
||||
ui_background_image_src: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"ui_background_image_src",
|
||||
),
|
||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||
finish_count: __sdk::__query_builder::Col::new(table_name, "finish_count"),
|
||||
updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"),
|
||||
published_at_micros: __sdk::__query_builder::Col::new(
|
||||
table_name,
|
||||
"published_at_micros",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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 super::bark_battle_gallery_view_row_type::BarkBattleGalleryViewRow;
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
/// Table handle for the table `bark_battle_gallery_view`.
|
||||
///
|
||||
/// Obtain a handle from the [`BarkBattleGalleryViewTableAccess::bark_battle_gallery_view`] method on [`super::RemoteTables`],
|
||||
/// like `ctx.db.bark_battle_gallery_view()`.
|
||||
///
|
||||
/// Users are encouraged not to explicitly reference this type,
|
||||
/// but to directly chain method calls,
|
||||
/// like `ctx.db.bark_battle_gallery_view().on_insert(...)`.
|
||||
pub struct BarkBattleGalleryViewTableHandle<'ctx> {
|
||||
imp: __sdk::TableHandle<BarkBattleGalleryViewRow>,
|
||||
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the table `bark_battle_gallery_view`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteTables`].
|
||||
pub trait BarkBattleGalleryViewTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Obtain a [`BarkBattleGalleryViewTableHandle`], which mediates access to the table `bark_battle_gallery_view`.
|
||||
fn bark_battle_gallery_view(&self) -> BarkBattleGalleryViewTableHandle<'_>;
|
||||
}
|
||||
|
||||
impl BarkBattleGalleryViewTableAccess for super::RemoteTables {
|
||||
fn bark_battle_gallery_view(&self) -> BarkBattleGalleryViewTableHandle<'_> {
|
||||
BarkBattleGalleryViewTableHandle {
|
||||
imp: self
|
||||
.imp
|
||||
.get_table::<BarkBattleGalleryViewRow>("bark_battle_gallery_view"),
|
||||
ctx: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BarkBattleGalleryViewInsertCallbackId(__sdk::CallbackId);
|
||||
pub struct BarkBattleGalleryViewDeleteCallbackId(__sdk::CallbackId);
|
||||
|
||||
impl<'ctx> __sdk::Table for BarkBattleGalleryViewTableHandle<'ctx> {
|
||||
type Row = BarkBattleGalleryViewRow;
|
||||
type EventContext = super::EventContext;
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.imp.count()
|
||||
}
|
||||
fn iter(&self) -> impl Iterator<Item = BarkBattleGalleryViewRow> + '_ {
|
||||
self.imp.iter()
|
||||
}
|
||||
|
||||
type InsertCallbackId = BarkBattleGalleryViewInsertCallbackId;
|
||||
|
||||
fn on_insert(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> BarkBattleGalleryViewInsertCallbackId {
|
||||
BarkBattleGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_insert(&self, callback: BarkBattleGalleryViewInsertCallbackId) {
|
||||
self.imp.remove_on_insert(callback.0)
|
||||
}
|
||||
|
||||
type DeleteCallbackId = BarkBattleGalleryViewDeleteCallbackId;
|
||||
|
||||
fn on_delete(
|
||||
&self,
|
||||
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||
) -> BarkBattleGalleryViewDeleteCallbackId {
|
||||
BarkBattleGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||
}
|
||||
|
||||
fn remove_on_delete(&self, callback: BarkBattleGalleryViewDeleteCallbackId) {
|
||||
self.imp.remove_on_delete(callback.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||
let _table =
|
||||
client_cache.get_or_make_table::<BarkBattleGalleryViewRow>("bark_battle_gallery_view");
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub(super) fn parse_table_update(
|
||||
raw_updates: __ws::v2::TableUpdate,
|
||||
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleGalleryViewRow>> {
|
||||
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||
__sdk::InternalError::failed_parse("TableUpdate<BarkBattleGalleryViewRow>", "TableUpdate")
|
||||
.with_cause(e)
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for query builder access to the table `BarkBattleGalleryViewRow`.
|
||||
///
|
||||
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||
pub trait bark_battle_gallery_viewQueryTableAccess {
|
||||
#[allow(non_snake_case)]
|
||||
/// Get a query builder for the table `BarkBattleGalleryViewRow`.
|
||||
fn bark_battle_gallery_view(&self) -> __sdk::__query_builder::Table<BarkBattleGalleryViewRow>;
|
||||
}
|
||||
|
||||
impl bark_battle_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor {
|
||||
fn bark_battle_gallery_view(&self) -> __sdk::__query_builder::Table<BarkBattleGalleryViewRow> {
|
||||
__sdk::__query_builder::Table::new("bark_battle_gallery_view")
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ pub struct BarkBattleRunSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub status: String,
|
||||
pub client_started_at_micros: i64,
|
||||
pub server_started_at_micros: i64,
|
||||
|
||||
@@ -13,7 +13,6 @@ pub struct BarkBattleRuntimeConfigSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub published_snapshot_json: String,
|
||||
pub published_at_micros: i64,
|
||||
|
||||
@@ -81,7 +81,9 @@ fn spacetime_metrics() -> &'static SpacetimeMetrics {
|
||||
read_duration_ms: meter
|
||||
.f64_histogram("genarrative.spacetime.read.duration_ms")
|
||||
.with_unit("ms")
|
||||
.with_description("SpacetimeDB local subscription cache read duration in milliseconds")
|
||||
.with_description(
|
||||
"SpacetimeDB local subscription cache read duration in milliseconds",
|
||||
)
|
||||
.build(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::*;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use sha2::{Digest, Sha256};
|
||||
use spacetimedb::AnonymousViewContext;
|
||||
|
||||
pub(crate) mod tables;
|
||||
mod types;
|
||||
@@ -8,6 +9,38 @@ mod types;
|
||||
pub use tables::*;
|
||||
pub use types::*;
|
||||
|
||||
/// Bark Battle 公开广场列表投影。
|
||||
///
|
||||
/// HTTP gallery 订阅该 public view 后读取本地 cache;view 只从已发布配置和统计投影
|
||||
/// 组装 v1 公开字段,避免每个公开列表请求重新调用 procedure 热路径。
|
||||
#[spacetimedb::view(accessor = bark_battle_gallery_view, public)]
|
||||
pub fn bark_battle_gallery_view(ctx: &AnonymousViewContext) -> Vec<BarkBattleGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
.db
|
||||
.bark_battle_published_config()
|
||||
.by_bark_battle_published_owner_user_id()
|
||||
.filter(""..)
|
||||
.filter_map(|row| match build_bark_battle_gallery_view_row(ctx, &row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"汪汪声浪公开广场 view 跳过损坏的作品投影 work_id={}: {}",
|
||||
row.work_id,
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.work_id.cmp(&right.work_id))
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_bark_battle_draft(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -106,21 +139,23 @@ fn create_bark_battle_draft_tx(
|
||||
let config = BarkBattleEditorConfigSnapshot {
|
||||
title: normalize_title(input.title.as_deref())?,
|
||||
description: normalize_optional_text(input.description.as_deref()),
|
||||
theme_preset: normalize_required_preset(&input.theme_preset, "theme_preset")?,
|
||||
player_dog_skin_preset: normalize_required_preset(
|
||||
&input.player_dog_skin_preset,
|
||||
"player_dog_skin_preset",
|
||||
theme_description: normalize_required_description(
|
||||
&input.theme_description,
|
||||
"theme_description",
|
||||
)?,
|
||||
opponent_dog_skin_preset: normalize_required_preset(
|
||||
&input.opponent_dog_skin_preset,
|
||||
"opponent_dog_skin_preset",
|
||||
player_image_description: normalize_required_description(
|
||||
&input.player_image_description,
|
||||
"player_image_description",
|
||||
)?,
|
||||
opponent_image_description: normalize_required_description(
|
||||
&input.opponent_image_description,
|
||||
"opponent_image_description",
|
||||
)?,
|
||||
onomatopoeia: Vec::new(),
|
||||
player_character_image_src: None,
|
||||
opponent_character_image_src: None,
|
||||
ui_background_image_src: None,
|
||||
bark_sound_src: None,
|
||||
difficulty_preset: normalize_difficulty(input.difficulty_preset.as_deref())?,
|
||||
leaderboard_enabled: input.leaderboard_enabled.unwrap_or(true),
|
||||
};
|
||||
let row = BarkBattleDraftConfigRow {
|
||||
draft_id: input.draft_id.clone(),
|
||||
@@ -129,7 +164,7 @@ fn create_bark_battle_draft_tx(
|
||||
config_version: 1,
|
||||
ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(),
|
||||
difficulty_preset: config.difficulty_preset.clone(),
|
||||
leaderboard_enabled: config.leaderboard_enabled,
|
||||
leaderboard_enabled: true,
|
||||
config_json: to_json_string(&config),
|
||||
editor_state_json: normalize_json_string(
|
||||
input.editor_state_json.as_deref(),
|
||||
@@ -151,10 +186,8 @@ fn update_bark_battle_draft_config_tx(
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let mut editor_config = parse_editor_config(&input.config_json)?;
|
||||
normalize_editor_config_snapshot(&mut editor_config)?;
|
||||
if editor_config.difficulty_preset != input.difficulty_preset
|
||||
|| editor_config.leaderboard_enabled != input.leaderboard_enabled
|
||||
{
|
||||
return Err("bark_battle config_json 与行字段不一致".to_string());
|
||||
if editor_config.difficulty_preset != input.difficulty_preset {
|
||||
return Err("bark_battle config_json 与 difficulty_preset 不匹配".to_string());
|
||||
}
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
let existing = ctx
|
||||
@@ -166,14 +199,14 @@ fn update_bark_battle_draft_config_tx(
|
||||
if existing.owner_user_id != input.owner_user_id || existing.work_id != input.work_id {
|
||||
return Err("bark_battle draft owner/work 不匹配".to_string());
|
||||
}
|
||||
if input.config_version <= existing.config_version {
|
||||
return Err("bark_battle draft config_version 必须递增".to_string());
|
||||
}
|
||||
let mut row = existing;
|
||||
row.config_version = input.config_version;
|
||||
// 中文注释:HTTP BFF 会先读缓存再发更新,订阅缓存可能短暂落后;
|
||||
// 这里按“至少递增 1”兜底,避免前端重复保存素材时被版本号误伤。
|
||||
row.config_version = input
|
||||
.config_version
|
||||
.max(row.config_version.saturating_add(1));
|
||||
row.ruleset_version = normalize_ruleset_version(&input.ruleset_version)?;
|
||||
row.difficulty_preset = normalize_difficulty(Some(&input.difficulty_preset))?;
|
||||
row.leaderboard_enabled = input.leaderboard_enabled;
|
||||
row.config_json = to_json_string(&editor_config);
|
||||
row.updated_at = updated_at;
|
||||
ctx.db
|
||||
@@ -200,6 +233,20 @@ fn publish_bark_battle_work_tx(
|
||||
if draft.owner_user_id != input.owner_user_id || draft.work_id != input.work_id {
|
||||
return Err("bark_battle draft owner/work 不匹配".to_string());
|
||||
}
|
||||
let published_snapshot_json = match input.published_snapshot_json.as_deref() {
|
||||
Some(value) => {
|
||||
let mut editor_config = parse_editor_config(value)?;
|
||||
normalize_editor_config_snapshot(&mut editor_config)?;
|
||||
if editor_config.difficulty_preset != draft.difficulty_preset {
|
||||
return Err(
|
||||
"bark_battle published_snapshot_json 与草稿 difficulty_preset 不匹配"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
to_json_string(&editor_config)
|
||||
}
|
||||
None => draft.config_json.clone(),
|
||||
};
|
||||
let published = BarkBattlePublishedConfigRow {
|
||||
work_id: draft.work_id.clone(),
|
||||
owner_user_id: draft.owner_user_id.clone(),
|
||||
@@ -207,12 +254,9 @@ fn publish_bark_battle_work_tx(
|
||||
config_version: draft.config_version,
|
||||
ruleset_version: normalize_ruleset_version(&draft.ruleset_version)?,
|
||||
difficulty_preset: normalize_difficulty(Some(&draft.difficulty_preset))?,
|
||||
leaderboard_enabled: draft.leaderboard_enabled,
|
||||
config_json: draft.config_json.clone(),
|
||||
published_snapshot_json: match input.published_snapshot_json.as_deref() {
|
||||
Some(value) => normalize_json_string(Some(value), "published_snapshot_json")?,
|
||||
None => draft.config_json.clone(),
|
||||
},
|
||||
leaderboard_enabled: true,
|
||||
config_json: published_snapshot_json.clone(),
|
||||
published_snapshot_json,
|
||||
created_at: published_at,
|
||||
updated_at: published_at,
|
||||
published_at,
|
||||
@@ -297,7 +341,7 @@ fn start_bark_battle_run_tx(
|
||||
config_version: input.config_version,
|
||||
ruleset_version: input.ruleset_version,
|
||||
difficulty_preset: input.difficulty_preset,
|
||||
leaderboard_enabled: published.leaderboard_enabled,
|
||||
leaderboard_enabled: true,
|
||||
status: BARK_BATTLE_RUN_RUNNING.to_string(),
|
||||
client_started_at_micros: input.client_started_at_micros,
|
||||
server_started_at: started_at,
|
||||
@@ -483,7 +527,6 @@ fn draft_snapshot(row: &BarkBattleDraftConfigRow) -> BarkBattleDraftConfigSnapsh
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
config_json: row.config_json.clone(),
|
||||
editor_state_json: row.editor_state_json.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
@@ -499,7 +542,6 @@ fn runtime_config_snapshot(row: &BarkBattlePublishedConfigRow) -> BarkBattleRunt
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
config_json: row.config_json.clone(),
|
||||
published_snapshot_json: row.published_snapshot_json.clone(),
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
@@ -507,6 +549,43 @@ fn runtime_config_snapshot(row: &BarkBattlePublishedConfigRow) -> BarkBattleRunt
|
||||
}
|
||||
}
|
||||
|
||||
fn build_bark_battle_gallery_view_row(
|
||||
ctx: &AnonymousViewContext,
|
||||
row: &BarkBattlePublishedConfigRow,
|
||||
) -> Result<BarkBattleGalleryViewRow, String> {
|
||||
let mut editor_config = parse_editor_config(&row.config_json)?;
|
||||
normalize_editor_config_snapshot(&mut editor_config)?;
|
||||
let stats = ctx
|
||||
.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.find(&row.work_id);
|
||||
Ok(BarkBattleGalleryViewRow {
|
||||
work_id: row.work_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_draft_id: row.source_draft_id.clone(),
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
title: editor_config.title,
|
||||
description: editor_config.description,
|
||||
theme_description: editor_config.theme_description,
|
||||
player_image_description: editor_config.player_image_description,
|
||||
opponent_image_description: editor_config.opponent_image_description,
|
||||
onomatopoeia: editor_config.onomatopoeia,
|
||||
player_character_image_src: editor_config.player_character_image_src,
|
||||
opponent_character_image_src: editor_config.opponent_character_image_src,
|
||||
ui_background_image_src: editor_config.ui_background_image_src,
|
||||
play_count: stats.as_ref().map(|stats| stats.play_count).unwrap_or(0),
|
||||
finish_count: stats
|
||||
.as_ref()
|
||||
.map(|stats| stats.finished_count)
|
||||
.unwrap_or(0),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
})
|
||||
}
|
||||
|
||||
fn hash_run_token(token: &str) -> String {
|
||||
let digest = Sha256::digest(token.as_bytes());
|
||||
digest.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
@@ -530,11 +609,17 @@ fn normalize_editor_config_snapshot(
|
||||
config: &mut BarkBattleEditorConfigSnapshot,
|
||||
) -> Result<(), String> {
|
||||
config.title = normalize_title(Some(&config.title))?;
|
||||
config.theme_preset = normalize_required_preset(&config.theme_preset, "theme_preset")?;
|
||||
config.player_dog_skin_preset =
|
||||
normalize_required_preset(&config.player_dog_skin_preset, "player_dog_skin_preset")?;
|
||||
config.opponent_dog_skin_preset =
|
||||
normalize_required_preset(&config.opponent_dog_skin_preset, "opponent_dog_skin_preset")?;
|
||||
config.theme_description =
|
||||
normalize_required_description(&config.theme_description, "theme_description")?;
|
||||
config.player_image_description = normalize_required_description(
|
||||
&config.player_image_description,
|
||||
"player_image_description",
|
||||
)?;
|
||||
config.opponent_image_description = normalize_required_description(
|
||||
&config.opponent_image_description,
|
||||
"opponent_image_description",
|
||||
)?;
|
||||
config.onomatopoeia = normalize_onomatopoeia(std::mem::take(&mut config.onomatopoeia));
|
||||
config.player_character_image_src = normalize_optional_asset_source(
|
||||
config.player_character_image_src.as_deref(),
|
||||
"player_character_image_src",
|
||||
@@ -547,8 +632,6 @@ fn normalize_editor_config_snapshot(
|
||||
config.ui_background_image_src.as_deref(),
|
||||
"ui_background_image_src",
|
||||
)?;
|
||||
config.bark_sound_src =
|
||||
normalize_optional_asset_source(config.bark_sound_src.as_deref(), "bark_sound_src")?;
|
||||
config.difficulty_preset = normalize_difficulty(Some(&config.difficulty_preset))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -568,12 +651,24 @@ fn normalize_optional_text(value: Option<&str>) -> String {
|
||||
value.unwrap_or_default().trim().chars().take(120).collect()
|
||||
}
|
||||
|
||||
fn normalize_required_preset(value: &str, field_name: &str) -> Result<String, String> {
|
||||
let preset = value.trim();
|
||||
if preset.is_empty() {
|
||||
fn normalize_required_description(value: &str, field_name: &str) -> Result<String, String> {
|
||||
let description = value.trim();
|
||||
if description.is_empty() {
|
||||
return Err(format!("bark_battle {field_name} 不能为空"));
|
||||
}
|
||||
Ok(preset.to_string())
|
||||
if description.chars().count() > 240 {
|
||||
return Err(format!("bark_battle {field_name} 不能超过 240 个字符"));
|
||||
}
|
||||
Ok(description.to_string())
|
||||
}
|
||||
|
||||
fn normalize_onomatopoeia(words: Vec<String>) -> Vec<String> {
|
||||
words
|
||||
.into_iter()
|
||||
.map(|word| word.trim().chars().take(12).collect::<String>())
|
||||
.filter(|word| !word.is_empty())
|
||||
.take(24)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn normalize_optional_asset_source(
|
||||
@@ -674,7 +769,6 @@ fn run_snapshot(row: &BarkBattleRuntimeRunRow) -> BarkBattleRunSnapshot {
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
status: row.status.clone(),
|
||||
client_started_at_micros: row.client_started_at_micros,
|
||||
server_started_at_micros: row.server_started_at.to_micros_since_unix_epoch(),
|
||||
@@ -905,7 +999,6 @@ mod tests {
|
||||
config_version: 1,
|
||||
ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(),
|
||||
difficulty_preset: BARK_BATTLE_DIFFICULTY_NORMAL.to_string(),
|
||||
leaderboard_enabled: true,
|
||||
config_json: "{}".to_string(),
|
||||
updated_at_micros: 1_700_000,
|
||||
};
|
||||
@@ -919,7 +1012,6 @@ mod tests {
|
||||
config_version: input.config_version,
|
||||
ruleset_version: input.ruleset_version.clone(),
|
||||
difficulty_preset: input.difficulty_preset.clone(),
|
||||
leaderboard_enabled: input.leaderboard_enabled,
|
||||
config_json: input.config_json.clone(),
|
||||
editor_state_json: "{}".to_string(),
|
||||
created_at_micros: 1_700_000,
|
||||
@@ -945,4 +1037,84 @@ mod tests {
|
||||
assert!(normalize_title(Some(" 标题 ")).is_ok());
|
||||
assert!(normalize_title(Some(" ")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn published_snapshot_is_normalized_as_runtime_config() {
|
||||
let mut editor_config = parse_editor_config(
|
||||
&serde_json::json!({
|
||||
"title": " 汪汪测试杯 ",
|
||||
"description": "",
|
||||
"themeDescription": " 阳光草坪 ",
|
||||
"playerImageDescription": " 主角柴犬 ",
|
||||
"opponentImageDescription": " 对手哈士奇 ",
|
||||
"onomatopoeia": [" 轰汪! ", "冲啊冲啊冲啊冲啊冲啊!", ""],
|
||||
"playerCharacterImageSrc": "/generated-bark-battle-assets/player.png",
|
||||
"opponentCharacterImageSrc": "/generated-bark-battle-assets/opponent.png",
|
||||
"uiBackgroundImageSrc": "/generated-bark-battle-assets/ui.png",
|
||||
"difficultyPreset": "normal"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("published snapshot should parse");
|
||||
|
||||
normalize_editor_config_snapshot(&mut editor_config)
|
||||
.expect("published snapshot should normalize");
|
||||
let config_json = to_json_string(&editor_config);
|
||||
|
||||
assert!(config_json.contains("/generated-bark-battle-assets/player.png"));
|
||||
assert!(config_json.contains("/generated-bark-battle-assets/opponent.png"));
|
||||
assert!(config_json.contains("/generated-bark-battle-assets/ui.png"));
|
||||
assert!(config_json.contains("阳光草坪"));
|
||||
assert!(config_json.contains("轰汪!"));
|
||||
assert!(config_json.contains("冲啊冲啊冲啊冲啊"));
|
||||
assert!(!config_json.contains("冲啊冲啊冲啊冲啊冲啊!"));
|
||||
assert!(!config_json.contains("\"title\":\" 汪汪测试杯 \""));
|
||||
assert!(!config_json.contains("themePreset"));
|
||||
assert!(!config_json.contains("playerDogSkinPreset"));
|
||||
assert!(!config_json.contains("opponentDogSkinPreset"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bark_battle_gallery_view_row_exposes_custom_onomatopoeia() {
|
||||
let mut editor_config = parse_editor_config(
|
||||
&serde_json::json!({
|
||||
"title": "声浪公开赛",
|
||||
"description": "画廊映射测试",
|
||||
"themeDescription": "霓虹竞技场",
|
||||
"playerImageDescription": "星际猫骑士",
|
||||
"opponentImageDescription": "机器人拳手",
|
||||
"onomatopoeia": [" 轰! ", "炸场!", ""],
|
||||
"difficultyPreset": "normal"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("gallery config should parse");
|
||||
|
||||
normalize_editor_config_snapshot(&mut editor_config)
|
||||
.expect("gallery config should normalize");
|
||||
|
||||
let row = BarkBattleGalleryViewRow {
|
||||
work_id: "BB-33333333".to_string(),
|
||||
owner_user_id: "user-3".to_string(),
|
||||
source_draft_id: Some("bark-battle-draft-3".to_string()),
|
||||
config_version: 1,
|
||||
ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(),
|
||||
difficulty_preset: editor_config.difficulty_preset.clone(),
|
||||
title: editor_config.title,
|
||||
description: editor_config.description,
|
||||
theme_description: editor_config.theme_description,
|
||||
player_image_description: editor_config.player_image_description,
|
||||
opponent_image_description: editor_config.opponent_image_description,
|
||||
onomatopoeia: editor_config.onomatopoeia,
|
||||
player_character_image_src: editor_config.player_character_image_src,
|
||||
opponent_character_image_src: editor_config.opponent_character_image_src,
|
||||
ui_background_image_src: editor_config.ui_background_image_src,
|
||||
play_count: 8,
|
||||
finish_count: 5,
|
||||
updated_at_micros: 1_713_686_401_234_567,
|
||||
published_at_micros: 1_713_686_401_234_000,
|
||||
};
|
||||
|
||||
assert_eq!(row.onomatopoeia, vec!["轰!".to_string(), "炸场!".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,10 @@ pub struct BarkBattleDraftCreateInput {
|
||||
pub work_id: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
pub difficulty_preset: Option<String>,
|
||||
pub leaderboard_enabled: Option<bool>,
|
||||
pub editor_state_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
@@ -41,7 +40,6 @@ pub struct BarkBattleDraftConfigUpsertInput {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
@@ -116,19 +114,18 @@ pub struct BarkBattleProcedureResult {
|
||||
pub struct BarkBattleEditorConfigSnapshot {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub onomatopoeia: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ui_background_image_src: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bark_sound_src: Option<String>,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)]
|
||||
@@ -140,7 +137,6 @@ pub struct BarkBattleDraftConfigSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub editor_state_json: String,
|
||||
pub created_at_micros: i64,
|
||||
@@ -156,7 +152,6 @@ pub struct BarkBattleRuntimeConfigSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub published_snapshot_json: String,
|
||||
pub published_at_micros: i64,
|
||||
@@ -172,7 +167,6 @@ pub struct BarkBattleRunSnapshot {
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub status: String,
|
||||
pub client_started_at_micros: i64,
|
||||
pub server_started_at_micros: i64,
|
||||
@@ -185,3 +179,31 @@ pub struct BarkBattleRunSnapshot {
|
||||
pub leaderboard_score: Option<u64>,
|
||||
pub score_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Bark Battle 公开广场只读投影行。
|
||||
///
|
||||
/// 该结构只暴露 v1 公共卡片需要的配置和基础统计,不把内部排行榜开关或旧皮肤 /
|
||||
/// 音效预设重新带回公开语义。
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleGalleryViewRow {
|
||||
pub work_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_draft_id: Option<String>,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub theme_description: String,
|
||||
pub player_image_description: String,
|
||||
pub opponent_image_description: String,
|
||||
pub onomatopoeia: Vec<String>,
|
||||
pub player_character_image_src: Option<String>,
|
||||
pub opponent_character_image_src: Option<String>,
|
||||
pub ui_background_image_src: Option<String>,
|
||||
pub play_count: u64,
|
||||
pub finish_count: u64,
|
||||
pub updated_at_micros: i64,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
@@ -180,17 +180,7 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
|
||||
}
|
||||
|
||||
migrate_visual_novel_entry_from_old_visible_default(ctx, now);
|
||||
migrate_coming_soon_entry_from_old_open_default(
|
||||
ctx,
|
||||
now,
|
||||
ComingSoonEntryDefault {
|
||||
id: "bark-battle",
|
||||
title: "汪汪声浪",
|
||||
subtitle: "声控对战挑战",
|
||||
image_src: "/creation-type-references/creative-agent.webp",
|
||||
sort_order: 85,
|
||||
},
|
||||
);
|
||||
migrate_bark_battle_entry_to_open_default(ctx, now);
|
||||
migrate_coming_soon_entry_from_old_open_default(
|
||||
ctx,
|
||||
now,
|
||||
@@ -204,6 +194,36 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
|
||||
);
|
||||
}
|
||||
|
||||
fn migrate_bark_battle_entry_to_open_default(ctx: &ReducerContext, now: Timestamp) {
|
||||
let id = "bark-battle".to_string();
|
||||
let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// 中文注释:只纠偏系统默认汪汪声浪入口,不覆盖后台手动改过标题、排序或可见性的配置。
|
||||
let still_system_default = row.title == "汪汪声浪"
|
||||
&& row.subtitle == "声控对战挑战"
|
||||
&& row.visible
|
||||
&& row.sort_order == 85
|
||||
&& (row.image_src == "/creation-type-references/creative-agent.webp"
|
||||
|| row.image_src == "/creation-type-references/bark-battle.webp")
|
||||
&& ((row.badge == "敬请期待" && !row.open) || (row.badge == "可创建" && row.open));
|
||||
if !still_system_default {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.creation_entry_type_config()
|
||||
.id()
|
||||
.update(CreationEntryTypeConfig {
|
||||
badge: "可创建".to_string(),
|
||||
image_src: "/creation-type-references/bark-battle.webp".to_string(),
|
||||
open: true,
|
||||
updated_at: now,
|
||||
..row
|
||||
});
|
||||
}
|
||||
|
||||
fn migrate_visual_novel_entry_from_old_visible_default(ctx: &ReducerContext, now: Timestamp) {
|
||||
let id = "visual-novel".to_string();
|
||||
let Some(row) = ctx.db.creation_entry_type_config().id().find(&id) else {
|
||||
|
||||
Reference in New Issue
Block a user