Files
Genarrative/server-rs/crates/spacetime-module/src/public_work.rs

789 lines
29 KiB
Rust

use crate::puzzle::{PuzzleGalleryCardViewRow, puzzle_gallery_card_view, puzzle_gallery_view};
use crate::*;
use module_custom_world::{CustomWorldGalleryEntrySnapshot, CustomWorldProfileSnapshot};
use module_puzzle::PuzzleWorkProfile;
use spacetimedb::AnonymousViewContext;
/// 跨玩法公开作品列表卡片读模型。
///
/// 该 view 只收口平台公开列表所需字段;玩法专属 runtime 配置仍留在各玩法详情 /
/// runtime procedure 中读取。
#[spacetimedb::view(accessor = public_work_gallery_entry, public)]
pub fn public_work_gallery_entry(ctx: &AnonymousViewContext) -> Vec<PublicWorkGalleryEntry> {
let mut entries = Vec::new();
entries.extend(
puzzle_gallery_card_view(ctx)
.into_iter()
.map(map_puzzle_gallery_entry),
);
entries.extend(
custom_world_public_gallery_snapshots(ctx)
.into_iter()
.map(map_custom_world_gallery_entry),
);
entries.extend(
jump_hop_gallery_card_view(ctx)
.into_iter()
.map(map_jump_hop_gallery_entry),
);
entries.extend(
wooden_fish_gallery_card_view(ctx)
.into_iter()
.map(map_wooden_fish_gallery_entry),
);
entries.extend(
match3d_gallery_view(ctx)
.into_iter()
.map(map_match3d_gallery_entry),
);
entries.extend(
square_hole_gallery_view(ctx)
.into_iter()
.map(map_square_hole_gallery_entry),
);
entries.extend(
visual_novel_gallery_view(ctx)
.into_iter()
.map(map_visual_novel_gallery_entry),
);
entries.extend(
big_fish_gallery_view(ctx)
.into_iter()
.map(map_big_fish_gallery_entry),
);
entries.extend(
bark_battle_gallery_view(ctx)
.into_iter()
.map(map_bark_battle_gallery_entry),
);
sort_public_work_gallery_entries(&mut entries);
entries
}
/// 跨玩法公开作品详情摘要读模型。
///
/// `detail_payload_json` 只承载平台详情页展示扩展字段,不承载正式 runtime 配置。
#[spacetimedb::view(accessor = public_work_detail_entry, public)]
pub fn public_work_detail_entry(ctx: &AnonymousViewContext) -> Vec<PublicWorkDetailEntry> {
let mut entries = Vec::new();
entries.extend(
puzzle_gallery_view(ctx)
.into_iter()
.map(map_puzzle_detail_entry),
);
entries.extend(
custom_world_public_profile_snapshots(ctx)
.into_iter()
.map(map_custom_world_detail_entry),
);
entries.extend(
jump_hop_gallery_view(ctx)
.into_iter()
.map(map_jump_hop_detail_entry),
);
entries.extend(
wooden_fish_gallery_view(ctx)
.into_iter()
.map(map_wooden_fish_detail_entry),
);
entries.extend(
match3d_gallery_view(ctx)
.into_iter()
.map(map_match3d_detail_entry),
);
entries.extend(
square_hole_gallery_view(ctx)
.into_iter()
.map(map_square_hole_detail_entry),
);
entries.extend(
visual_novel_gallery_view(ctx)
.into_iter()
.map(map_visual_novel_detail_entry),
);
entries.extend(
big_fish_gallery_view(ctx)
.into_iter()
.map(map_big_fish_detail_entry),
);
entries.extend(
bark_battle_gallery_view(ctx)
.into_iter()
.map(map_bark_battle_detail_entry),
);
entries.sort_by(|left, right| {
right
.sort_time_micros
.cmp(&left.sort_time_micros)
.then_with(|| left.source_type.cmp(&right.source_type))
.then_with(|| left.profile_id.cmp(&right.profile_id))
});
entries
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct PublicWorkGalleryEntry {
pub source_type: String,
pub work_id: String,
pub profile_id: String,
pub source_session_id: Option<String>,
pub public_work_code: String,
pub owner_user_id: String,
pub author_display_name: String,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub theme_tags: Vec<String>,
pub play_count: u32,
pub remix_count: u32,
pub like_count: u32,
pub published_at_micros: Option<i64>,
pub updated_at_micros: i64,
pub sort_time_micros: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct PublicWorkDetailEntry {
pub source_type: String,
pub work_id: String,
pub profile_id: String,
pub source_session_id: Option<String>,
pub public_work_code: String,
pub owner_user_id: String,
pub author_display_name: String,
pub world_name: String,
pub subtitle: String,
pub summary_text: String,
pub cover_image_src: Option<String>,
pub cover_asset_id: Option<String>,
pub theme_tags: Vec<String>,
pub play_count: u32,
pub remix_count: u32,
pub like_count: u32,
pub published_at_micros: Option<i64>,
pub updated_at_micros: i64,
pub sort_time_micros: i64,
pub detail_payload_json: String,
}
fn sort_public_work_gallery_entries(entries: &mut [PublicWorkGalleryEntry]) {
entries.sort_by(|left, right| {
right
.sort_time_micros
.cmp(&left.sort_time_micros)
.then_with(|| left.source_type.cmp(&right.source_type))
.then_with(|| left.profile_id.cmp(&right.profile_id))
});
}
fn gallery_to_detail(
entry: PublicWorkGalleryEntry,
detail_payload_json: String,
) -> PublicWorkDetailEntry {
PublicWorkDetailEntry {
source_type: entry.source_type,
work_id: entry.work_id,
profile_id: entry.profile_id,
source_session_id: entry.source_session_id,
public_work_code: entry.public_work_code,
owner_user_id: entry.owner_user_id,
author_display_name: entry.author_display_name,
world_name: entry.world_name,
subtitle: entry.subtitle,
summary_text: entry.summary_text,
cover_image_src: entry.cover_image_src,
cover_asset_id: entry.cover_asset_id,
theme_tags: entry.theme_tags,
play_count: entry.play_count,
remix_count: entry.remix_count,
like_count: entry.like_count,
published_at_micros: entry.published_at_micros,
updated_at_micros: entry.updated_at_micros,
sort_time_micros: entry.sort_time_micros,
detail_payload_json,
}
}
fn map_puzzle_gallery_entry(row: PuzzleGalleryCardViewRow) -> PublicWorkGalleryEntry {
let world_name = choose_non_empty(&[row.work_title.as_str(), row.level_name.as_str()]);
let summary_text = choose_non_empty(&[row.work_description.as_str(), row.summary.as_str()]);
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "puzzle".to_string(),
work_id: row.work_id,
profile_id: row.profile_id.clone(),
source_session_id: row.source_session_id,
public_work_code: build_prefixed_public_work_code("PZ", &row.profile_id),
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name,
subtitle: "拼图关卡".to_string(),
summary_text,
cover_image_src: row.cover_image_src,
cover_asset_id: row.cover_asset_id,
theme_tags: row.theme_tags,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_puzzle_detail_entry(row: PuzzleWorkProfile) -> PublicWorkDetailEntry {
let entry = map_puzzle_gallery_entry(PuzzleGalleryCardViewRow {
work_id: row.work_id,
profile_id: row.profile_id,
owner_user_id: row.owner_user_id,
source_session_id: row.source_session_id,
author_display_name: row.author_display_name,
work_title: row.work_title,
work_description: row.work_description,
level_name: row.level_name,
summary: row.summary,
theme_tags: row.theme_tags,
cover_image_src: row.cover_image_src,
cover_asset_id: row.cover_asset_id,
publication_status: row.publication_status,
updated_at_micros: row.updated_at_micros,
published_at_micros: row.published_at_micros,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row.point_incentive_claimed_points,
publish_ready: row.publish_ready,
generation_status: None,
});
let detail_payload_json = json_string(json!({
"sourceType": "puzzle",
"levelCount": row.levels.len(),
"coverSlides": row.levels.iter().filter_map(|level| {
level.cover_image_src.as_ref().map(|image_src| json!({
"id": level.level_id,
"imageSrc": image_src,
"label": level.level_name,
}))
}).collect::<Vec<_>>(),
}));
gallery_to_detail(entry, detail_payload_json)
}
fn map_custom_world_gallery_entry(row: CustomWorldGalleryEntrySnapshot) -> PublicWorkGalleryEntry {
PublicWorkGalleryEntry {
source_type: "custom-world".to_string(),
work_id: format!("custom-world-work-{}", row.profile_id),
profile_id: row.profile_id,
source_session_id: None,
public_work_code: row.public_work_code,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.world_name,
subtitle: row.subtitle,
summary_text: row.summary_text,
cover_image_src: row.cover_image_src,
cover_asset_id: None,
theme_tags: vec![format_custom_world_theme_mode(row.theme_mode).to_string()],
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
published_at_micros: Some(row.published_at_micros),
updated_at_micros: row.updated_at_micros,
sort_time_micros: row.published_at_micros,
}
}
fn map_custom_world_detail_entry(row: CustomWorldProfileSnapshot) -> PublicWorkDetailEntry {
let public_work_code = row
.public_work_code
.clone()
.unwrap_or_else(|| custom_world::build_custom_world_public_work_code(&row.profile_id));
let published_at_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
let entry = PublicWorkGalleryEntry {
source_type: "custom-world".to_string(),
work_id: format!("custom-world-work-{}", row.profile_id),
profile_id: row.profile_id,
source_session_id: row.source_agent_session_id.clone(),
public_work_code,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.world_name,
subtitle: row.subtitle,
summary_text: row.summary_text,
cover_image_src: row.cover_image_src,
cover_asset_id: None,
theme_tags: vec![format_custom_world_theme_mode(row.theme_mode).to_string()],
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
published_at_micros: Some(published_at_micros),
updated_at_micros: row.updated_at_micros,
sort_time_micros: published_at_micros,
};
let detail_payload_json = json_string(json!({
"sourceType": "custom-world",
"authorPublicUserCode": row.author_public_user_code,
"sourceAgentSessionId": row.source_agent_session_id,
"themeMode": format_custom_world_theme_mode(row.theme_mode),
"playableNpcCount": row.playable_npc_count,
"landmarkCount": row.landmark_count,
}));
gallery_to_detail(entry, detail_payload_json)
}
fn map_jump_hop_gallery_entry(row: JumpHopGalleryCardViewRow) -> PublicWorkGalleryEntry {
let subtitle = jump_hop_difficulty_label(&row.difficulty).to_string();
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "jump-hop".to_string(),
work_id: row.work_id,
profile_id: row.profile_id,
source_session_id: None,
public_work_code: row.public_work_code,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.work_title,
subtitle,
summary_text: row.work_description,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: None,
theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_jump_hop_detail_entry(row: JumpHopGalleryViewRow) -> PublicWorkDetailEntry {
let entry = PublicWorkGalleryEntry {
source_type: "jump-hop".to_string(),
work_id: row.work_id,
profile_id: row.profile_id.clone(),
source_session_id: empty_string_to_option(row.source_session_id.clone()),
public_work_code: build_prefixed_public_work_code("JH", &row.profile_id),
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.work_title,
subtitle: jump_hop_difficulty_label(&row.difficulty).to_string(),
summary_text: row.work_description,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: None,
theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros: row.published_at_micros.unwrap_or(row.updated_at_micros),
};
let detail_payload_json = json_string(json!({
"sourceType": "jump-hop",
"difficulty": row.difficulty,
"stylePreset": row.style_preset,
"tileAssetCount": row.tile_assets.len(),
"platformCount": row.path.platforms.len(),
"generationStatus": row.generation_status,
}));
gallery_to_detail(entry, detail_payload_json)
}
fn map_wooden_fish_gallery_entry(row: WoodenFishGalleryCardViewRow) -> PublicWorkGalleryEntry {
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "wooden-fish".to_string(),
work_id: row.work_id,
profile_id: row.profile_id,
source_session_id: None,
public_work_code: row.public_work_code,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.work_title,
subtitle: "敲木鱼".to_string(),
summary_text: row.work_description,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: None,
theme_tags: fallback_tags(row.theme_tags, &["敲木鱼"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_wooden_fish_detail_entry(row: WoodenFishGalleryViewRow) -> PublicWorkDetailEntry {
let entry = PublicWorkGalleryEntry {
source_type: "wooden-fish".to_string(),
work_id: row.work_id,
profile_id: row.profile_id,
source_session_id: empty_string_to_option(row.source_session_id),
public_work_code: row.public_work_code,
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.work_title,
subtitle: "敲木鱼".to_string(),
summary_text: row.work_description,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: None,
theme_tags: fallback_tags(row.theme_tags, &["敲木鱼"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros: row.published_at_micros.unwrap_or(row.updated_at_micros),
};
let detail_payload_json = json_string(json!({
"sourceType": "wooden-fish",
"hitObjectPrompt": row.hit_object_prompt,
"floatingWords": row.floating_words,
"generationStatus": row.generation_status,
"hasBackgroundAsset": row.background_asset.is_some(),
"hasHitSoundAsset": row.hit_sound_asset.is_some(),
}));
gallery_to_detail(entry, detail_payload_json)
}
fn map_match3d_gallery_entry(row: Match3DGalleryViewRow) -> PublicWorkGalleryEntry {
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "match3d".to_string(),
work_id: row.profile_id.clone(),
profile_id: row.profile_id.clone(),
source_session_id: empty_string_to_option(row.source_session_id),
public_work_code: build_prefixed_public_work_code("M3", &row.profile_id),
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.game_name,
subtitle: "经典消除玩法".to_string(),
summary_text: row.summary_text,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: empty_string_to_option(row.cover_asset_id),
theme_tags: fallback_tags(row.tags, &[row.theme_text.as_str(), "抓大鹅"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_match3d_detail_entry(row: Match3DGalleryViewRow) -> PublicWorkDetailEntry {
let detail_payload_json = json_string(json!({
"sourceType": "match3d",
"themeText": row.theme_text,
"referenceImageSrc": row.reference_image_src,
"clearCount": row.clear_count,
"difficulty": row.difficulty,
"generatedItemAssetsReady": row.generated_item_assets_json.as_ref().is_some_and(|value| !value.trim().is_empty()),
}));
gallery_to_detail(map_match3d_gallery_entry(row), detail_payload_json)
}
fn map_square_hole_gallery_entry(row: SquareHoleGalleryViewRow) -> PublicWorkGalleryEntry {
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "square-hole".to_string(),
work_id: row.work_id,
profile_id: row.profile_id.clone(),
source_session_id: empty_string_to_option(row.source_session_id),
public_work_code: build_prefixed_public_work_code("SH", &row.profile_id),
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.game_name,
subtitle: choose_non_empty(&[row.twist_rule.as_str(), "反直觉形状分拣"]),
summary_text: row.summary_text,
cover_image_src: empty_string_to_option(row.cover_image_src),
cover_asset_id: None,
theme_tags: fallback_tags(row.tags, &[row.theme_text.as_str(), "方洞挑战"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_square_hole_detail_entry(row: SquareHoleGalleryViewRow) -> PublicWorkDetailEntry {
let detail_payload_json = json_string(json!({
"sourceType": "square-hole",
"themeText": row.theme_text,
"twistRule": row.twist_rule,
"backgroundPrompt": row.background_prompt,
"backgroundImageSrc": empty_string_to_option(row.background_image_src.clone()),
"shapeCount": row.shape_count,
"difficulty": row.difficulty,
"shapeOptionCount": row.shape_options.len(),
"holeOptionCount": row.hole_options.len(),
}));
gallery_to_detail(map_square_hole_gallery_entry(row), detail_payload_json)
}
fn map_visual_novel_gallery_entry(row: VisualNovelGalleryViewRow) -> PublicWorkGalleryEntry {
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "visual-novel".to_string(),
work_id: row.work_id,
profile_id: row.profile_id.clone(),
source_session_id: row.source_session_id,
public_work_code: build_prefixed_public_work_code("VN", &row.profile_id),
owner_user_id: row.owner_user_id,
author_display_name: row.author_display_name,
world_name: row.work_title,
subtitle: "视觉小说模板".to_string(),
summary_text: row.work_description,
cover_image_src: row.cover_image_src,
cover_asset_id: None,
theme_tags: fallback_tags(row.tags, &["视觉小说"]),
play_count: row.play_count,
remix_count: 0,
like_count: 0,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_visual_novel_detail_entry(row: VisualNovelGalleryViewRow) -> PublicWorkDetailEntry {
let detail_payload_json = json_string(json!({
"sourceType": "visual-novel",
"sourceAssetIds": row.source_asset_ids,
"createdAtMicros": row.created_at_micros,
}));
gallery_to_detail(map_visual_novel_gallery_entry(row), detail_payload_json)
}
fn map_big_fish_gallery_entry(row: BigFishWorkSummarySnapshot) -> PublicWorkGalleryEntry {
let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros);
PublicWorkGalleryEntry {
source_type: "big-fish".to_string(),
work_id: row.work_id,
profile_id: row.source_session_id.clone(),
source_session_id: Some(row.source_session_id.clone()),
public_work_code: build_prefixed_public_work_code("BF", &row.source_session_id),
owner_user_id: row.owner_user_id,
author_display_name: "玩家".to_string(),
world_name: row.title,
subtitle: choose_non_empty(&[row.subtitle.as_str(), "大鱼吃小鱼"]),
summary_text: row.summary,
cover_image_src: row.cover_image_src,
cover_asset_id: None,
theme_tags: vec!["大鱼".to_string(), format!("{}", row.level_count)],
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
published_at_micros: row.published_at_micros,
updated_at_micros: row.updated_at_micros,
sort_time_micros,
}
}
fn map_big_fish_detail_entry(row: BigFishWorkSummarySnapshot) -> PublicWorkDetailEntry {
let detail_payload_json = json_string(json!({
"sourceType": "big-fish",
"status": row.status,
"publishReady": row.publish_ready,
"levelCount": row.level_count,
"levelMainImageReadyCount": row.level_main_image_ready_count,
"levelMotionReadyCount": row.level_motion_ready_count,
"backgroundReady": row.background_ready,
}));
gallery_to_detail(map_big_fish_gallery_entry(row), detail_payload_json)
}
fn map_bark_battle_gallery_entry(row: BarkBattleGalleryViewRow) -> PublicWorkGalleryEntry {
let cover_image_src = row
.ui_background_image_src
.clone()
.or_else(|| row.player_character_image_src.clone())
.or_else(|| row.opponent_character_image_src.clone())
.or_else(|| Some("/creation-type-references/bark-battle.webp".to_string()));
PublicWorkGalleryEntry {
source_type: "bark-battle".to_string(),
work_id: row.work_id.clone(),
profile_id: row.work_id.clone(),
source_session_id: row.source_draft_id,
public_work_code: build_bark_battle_public_work_code(&row.work_id),
owner_user_id: row.owner_user_id,
author_display_name: "玩家".to_string(),
world_name: choose_non_empty(&[row.title.as_str(), "汪汪声浪大作战"]),
subtitle: format!(
"汪汪声浪 · {}",
bark_battle_difficulty_label(&row.difficulty_preset)
),
summary_text: choose_non_empty(&[
row.description.as_str(),
row.theme_description.as_str(),
"用声音能量挑战对手。",
]),
cover_image_src,
cover_asset_id: None,
theme_tags: vec![
"汪汪声浪".to_string(),
bark_battle_difficulty_label(&row.difficulty_preset).to_string(),
],
play_count: saturating_u64_to_u32(row.play_count),
remix_count: 0,
like_count: 0,
published_at_micros: Some(row.published_at_micros),
updated_at_micros: row.updated_at_micros,
sort_time_micros: row.published_at_micros,
}
}
fn map_bark_battle_detail_entry(row: BarkBattleGalleryViewRow) -> PublicWorkDetailEntry {
let detail_payload_json = json_string(json!({
"sourceType": "bark-battle",
"difficultyPreset": row.difficulty_preset,
"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,
"finishCount": row.finish_count,
}));
gallery_to_detail(map_bark_battle_gallery_entry(row), detail_payload_json)
}
fn build_prefixed_public_work_code(prefix: &str, value: &str) -> String {
let normalized = normalize_public_code_text(value);
let fallback = if normalized.is_empty() {
"00000000".to_string()
} else {
normalized
};
let suffix = last_eight_padded(&fallback);
format!("{prefix}-{suffix}")
}
fn build_bark_battle_public_work_code(work_id: &str) -> String {
let normalized = normalize_public_code_text(work_id);
let without_prefix = normalized
.strip_prefix("BB")
.map(ToString::to_string)
.unwrap_or_else(|| normalized.clone());
let fallback = if without_prefix.is_empty() {
if normalized.is_empty() {
"00000000".to_string()
} else {
normalized
}
} else {
without_prefix
};
format!("BB-{}", last_eight_padded(&fallback))
}
fn normalize_public_code_text(value: &str) -> String {
value
.trim()
.chars()
.filter(|character| character.is_ascii_alphanumeric())
.flat_map(char::to_uppercase)
.collect()
}
fn last_eight_padded(value: &str) -> String {
let suffix = value
.chars()
.rev()
.take(8)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<String>();
format!("{suffix:0>8}")
}
fn choose_non_empty(values: &[&str]) -> String {
values
.iter()
.map(|value| value.trim())
.find(|value| !value.is_empty())
.unwrap_or_default()
.to_string()
}
fn empty_string_to_option(value: String) -> Option<String> {
let value = value.trim().to_string();
(!value.is_empty()).then_some(value)
}
fn fallback_tags(values: Vec<String>, fallback: &[&str]) -> Vec<String> {
let normalized = values
.into_iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect::<Vec<_>>();
if normalized.is_empty() {
fallback
.iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect()
} else {
normalized
}
}
fn jump_hop_difficulty_label(value: &str) -> &'static str {
match value.trim() {
"easy" => "轻松节奏",
"advanced" => "进阶跳台",
"challenge" => "极限路线",
_ => "标准路线",
}
}
fn bark_battle_difficulty_label(value: &str) -> &'static str {
match value.trim() {
"easy" => "轻松",
"hard" => "高能",
_ => "普通",
}
}
fn format_custom_world_theme_mode(value: CustomWorldThemeMode) -> &'static str {
match value {
CustomWorldThemeMode::Martial => "martial",
CustomWorldThemeMode::Arcane => "arcane",
CustomWorldThemeMode::Machina => "machina",
CustomWorldThemeMode::Tide => "tide",
CustomWorldThemeMode::Rift => "rift",
CustomWorldThemeMode::Mythic => "mythic",
}
}
fn saturating_u64_to_u32(value: u64) -> u32 {
value.min(u64::from(u32::MAX)) as u32
}
fn json_string(value: JsonValue) -> String {
serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string())
}