Files
Genarrative/server-rs/crates/spacetime-client/src/mapper/runtime.rs
T
JenkenB 50a0d6f982 Refine creation tab UX, generation flow, and bindings
Large changes across frontend, backend and docs to align creation-tab and generation-page behavior with new product UI/UX and Spacetime bindings. Updated hermes decision-log and pitfalls with concrete rules (banner carousel, font sizing, unread-dot tokens, template-card layout, direct card->entry routing, separation of account balance vs prize pools, removal of global page card shell, generation progress milestones and unified circular progress, and background video handling). Added GenerationProgressHero component and media assets, plus generation-related UI/tests updates (CustomWorldGenerationView, BarkBattleGeneratingView, creation hub/cards, platform entry routing, index tests). Backend and contract updates include new category fields in admin API types and admin UI form/list, spacetime-client/module/migration changes and generated bindings script. Misc: many tests adjusted, new docs and plan files added, and several server-rs crate changes to support the updated creation/ generation workflows.
2026-05-25 00:41:30 +08:00

521 lines
18 KiB
Rust

use super::*;
impl From<module_runtime::CreationEntryTypeAdminUpsertInput> for CreationEntryTypeAdminUpsertInput {
fn from(input: module_runtime::CreationEntryTypeAdminUpsertInput) -> Self {
Self {
id: input.id,
title: input.title,
subtitle: input.subtitle,
badge: input.badge,
image_src: input.image_src,
visible: input.visible,
open: input.open,
sort_order: input.sort_order,
category_id: input.category_id,
category_label: input.category_label,
category_sort_order: input.category_sort_order,
}
}
}
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeSettingUpsertInput> for RuntimeSettingUpsertInput {
fn from(input: module_runtime::RuntimeSettingUpsertInput) -> Self {
Self {
user_id: input.user_id,
music_volume: input.music_volume,
platform_theme: map_runtime_platform_theme(input.platform_theme),
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeBrowseHistoryListInput> for RuntimeBrowseHistoryListInput {
fn from(input: module_runtime::RuntimeBrowseHistoryListInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeBrowseHistoryClearInput> for RuntimeBrowseHistoryClearInput {
fn from(input: module_runtime::RuntimeBrowseHistoryClearInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeBrowseHistorySyncInput> for RuntimeBrowseHistorySyncInput {
fn from(input: module_runtime::RuntimeBrowseHistorySyncInput) -> Self {
Self {
user_id: input.user_id,
entries: input.entries.into_iter().map(Into::into).collect(),
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeBrowseHistoryWriteInput> for RuntimeBrowseHistoryWriteInput {
fn from(input: module_runtime::RuntimeBrowseHistoryWriteInput) -> Self {
Self {
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
world_name: input.world_name,
subtitle: input.subtitle,
summary_text: input.summary_text,
cover_image_src: input.cover_image_src,
theme_mode: input.theme_mode,
author_display_name: input.author_display_name,
visited_at: input.visited_at,
}
}
}
impl From<module_runtime::RuntimeSnapshotGetInput> for RuntimeSnapshotGetInput {
fn from(input: module_runtime::RuntimeSnapshotGetInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeSnapshotDeleteInput> for RuntimeSnapshotDeleteInput {
fn from(input: module_runtime::RuntimeSnapshotDeleteInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeTrackingEventInput> for RuntimeTrackingEventInput {
fn from(input: module_runtime::RuntimeTrackingEventInput) -> Self {
Self {
event_id: input.event_id,
event_key: input.event_key,
scope_kind: map_runtime_tracking_scope_kind(input.scope_kind),
scope_id: input.scope_id,
user_id: input.user_id,
owner_user_id: input.owner_user_id,
profile_id: input.profile_id,
module_key: input.module_key,
metadata_json: input.metadata_json,
occurred_at_micros: input.occurred_at_micros,
}
}
}
pub type CreationEntryConfigRecord =
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
pub(crate) fn map_creation_entry_config_procedure_result(
result: CreationEntryConfigProcedureResult,
) -> Result<CreationEntryConfigRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?;
Ok(module_runtime::build_creation_entry_config_response(
map_creation_entry_config_snapshot(snapshot),
))
}
pub(crate) fn build_creation_entry_config_record_from_rows(
header: CreationEntryConfig,
mut creation_types: Vec<CreationEntryTypeConfig>,
) -> CreationEntryConfigRecord {
creation_types.sort_by(|left, right| {
left.sort_order
.cmp(&right.sort_order)
.then_with(|| left.id.cmp(&right.id))
});
module_runtime::build_creation_entry_config_response(
module_runtime::CreationEntryConfigSnapshot {
config_id: header.config_id,
start_card: module_runtime::CreationEntryStartCardSnapshot {
title: header.start_title,
description: header.start_description,
idle_badge: header.start_idle_badge,
busy_badge: header.start_busy_badge,
},
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
title: header.modal_title,
description: header.modal_description,
},
event_banner: module_runtime::CreationEntryEventBannerSnapshot {
title: creation_entry_text_or_default(
header.event_title,
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_TITLE,
),
description: creation_entry_text_or_default(
header.event_description,
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION,
),
cover_image_src: creation_entry_text_or_default(
header.event_cover_image_src,
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC,
),
prize_pool_mud_points: header.event_prize_pool_mud_points,
starts_at_text: creation_entry_text_or_default(
header.event_starts_at_text,
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT,
),
ends_at_text: creation_entry_text_or_default(
header.event_ends_at_text,
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT,
),
},
creation_types: creation_types
.into_iter()
.map(|item| module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
category_id: creation_entry_text_or_default(
item.category_id,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_ID,
),
category_label: creation_entry_text_or_default(
item.category_label,
module_runtime::DEFAULT_CREATION_ENTRY_CATEGORY_LABEL,
),
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at.to_micros_since_unix_epoch(),
})
.collect(),
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
},
)
}
fn map_creation_entry_config_snapshot(
snapshot: CreationEntryConfigSnapshot,
) -> module_runtime::CreationEntryConfigSnapshot {
module_runtime::CreationEntryConfigSnapshot {
config_id: snapshot.config_id,
start_card: module_runtime::CreationEntryStartCardSnapshot {
title: snapshot.start_card.title,
description: snapshot.start_card.description,
idle_badge: snapshot.start_card.idle_badge,
busy_badge: snapshot.start_card.busy_badge,
},
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
title: snapshot.type_modal.title,
description: snapshot.type_modal.description,
},
event_banner: module_runtime::CreationEntryEventBannerSnapshot {
title: snapshot.event_banner.title,
description: snapshot.event_banner.description,
cover_image_src: snapshot.event_banner.cover_image_src,
prize_pool_mud_points: snapshot.event_banner.prize_pool_mud_points,
starts_at_text: snapshot.event_banner.starts_at_text,
ends_at_text: snapshot.event_banner.ends_at_text,
},
creation_types: snapshot
.creation_types
.into_iter()
.map(|item| module_runtime::CreationEntryTypeSnapshot {
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
image_src: item.image_src,
visible: item.visible,
open: item.open,
sort_order: item.sort_order,
category_id: item.category_id,
category_label: item.category_label,
category_sort_order: item.category_sort_order,
updated_at_micros: item.updated_at_micros,
})
.collect(),
updated_at_micros: snapshot.updated_at_micros,
}
}
fn creation_entry_text_or_default(value: Option<String>, default_value: &str) -> String {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| default_value.to_string())
}
pub(crate) fn map_runtime_setting_procedure_result(
result: RuntimeSettingProcedureResult,
) -> Result<RuntimeSettingsRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let snapshot = result
.record
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime settings 快照"))?;
Ok(build_runtime_setting_record(map_runtime_setting_snapshot(
snapshot,
)))
}
pub(crate) fn map_runtime_tracking_event_procedure_result(
result: RuntimeTrackingEventProcedureResult,
) -> Result<(), SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(())
}
pub(crate) fn map_runtime_tracking_event_batch_procedure_result(
result: RuntimeTrackingEventBatchProcedureResult,
) -> Result<u32, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result.accepted_count)
}
pub(crate) fn map_runtime_snapshot_procedure_result(
result: RuntimeSnapshotProcedureResult,
) -> Result<Option<RuntimeSnapshotRecord>, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
result
.record
.map(|snapshot| {
build_runtime_snapshot_record(map_runtime_snapshot_snapshot(snapshot))
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))
})
.transpose()
}
pub(crate) fn map_runtime_snapshot_required_procedure_result(
result: RuntimeSnapshotProcedureResult,
) -> Result<RuntimeSnapshotRecord, SpacetimeClientError> {
map_runtime_snapshot_procedure_result(result)?
.ok_or_else(|| SpacetimeClientError::missing_snapshot("runtime snapshot 快照"))
}
pub(crate) fn map_runtime_snapshot_delete_procedure_result(
result: RuntimeSnapshotProcedureResult,
) -> Result<bool, SpacetimeClientError> {
map_runtime_snapshot_procedure_result(result).map(|record| record.is_some())
}
pub(crate) fn map_runtime_setting_snapshot(
snapshot: RuntimeSettingSnapshot,
) -> module_runtime::RuntimeSettingSnapshot {
module_runtime::RuntimeSettingSnapshot {
user_id: snapshot.user_id,
music_volume: snapshot.music_volume,
platform_theme: map_runtime_platform_theme_back(snapshot.platform_theme),
created_at_micros: snapshot.created_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_platform_theme(
value: DomainRuntimePlatformTheme,
) -> crate::module_bindings::RuntimePlatformTheme {
match value {
DomainRuntimePlatformTheme::Light => crate::module_bindings::RuntimePlatformTheme::Light,
DomainRuntimePlatformTheme::Dark => crate::module_bindings::RuntimePlatformTheme::Dark,
}
}
pub(crate) fn map_runtime_platform_theme_back(
value: crate::module_bindings::RuntimePlatformTheme,
) -> DomainRuntimePlatformTheme {
match value {
crate::module_bindings::RuntimePlatformTheme::Light => DomainRuntimePlatformTheme::Light,
crate::module_bindings::RuntimePlatformTheme::Dark => DomainRuntimePlatformTheme::Dark,
}
}
pub(crate) fn map_runtime_tracking_scope_kind(
value: DomainRuntimeTrackingScopeKind,
) -> crate::module_bindings::RuntimeTrackingScopeKind {
match value {
DomainRuntimeTrackingScopeKind::Site => {
crate::module_bindings::RuntimeTrackingScopeKind::Site
}
DomainRuntimeTrackingScopeKind::Work => {
crate::module_bindings::RuntimeTrackingScopeKind::Work
}
DomainRuntimeTrackingScopeKind::Module => {
crate::module_bindings::RuntimeTrackingScopeKind::Module
}
DomainRuntimeTrackingScopeKind::User => {
crate::module_bindings::RuntimeTrackingScopeKind::User
}
}
}
pub(crate) fn map_runtime_tracking_scope_kind_back(
value: crate::module_bindings::RuntimeTrackingScopeKind,
) -> DomainRuntimeTrackingScopeKind {
match value {
crate::module_bindings::RuntimeTrackingScopeKind::Site => {
DomainRuntimeTrackingScopeKind::Site
}
crate::module_bindings::RuntimeTrackingScopeKind::Work => {
DomainRuntimeTrackingScopeKind::Work
}
crate::module_bindings::RuntimeTrackingScopeKind::Module => {
DomainRuntimeTrackingScopeKind::Module
}
crate::module_bindings::RuntimeTrackingScopeKind::User => {
DomainRuntimeTrackingScopeKind::User
}
}
}
pub(crate) fn parse_json_value(
value: &str,
label: &str,
) -> Result<serde_json::Value, SpacetimeClientError> {
serde_json::from_str::<serde_json::Value>(value)
.map_err(|error| SpacetimeClientError::Runtime(format!("{label} 非法: {error}")))
}
pub(crate) fn parse_json_array(
value: &str,
label: &str,
) -> Result<Vec<serde_json::Value>, SpacetimeClientError> {
match parse_json_value(value, label)? {
serde_json::Value::Array(entries) => Ok(entries),
_ => Err(SpacetimeClientError::Runtime(format!(
"{label} 必须是 JSON array"
))),
}
}
pub(crate) fn parse_json_string_array(
value: &str,
label: &str,
) -> Result<Vec<String>, SpacetimeClientError> {
parse_json_array(value, label)?
.into_iter()
.map(|entry| match entry {
serde_json::Value::String(value) => Ok(value),
_ => Err(SpacetimeClientError::Runtime(format!(
"{label} 必须是 string array"
))),
})
.collect()
}
pub(crate) fn parse_supported_actions_json(
value: &str,
) -> Result<Vec<CustomWorldSupportedActionRecord>, SpacetimeClientError> {
parse_json_array(value, "custom world agent supported_actions_json")?
.into_iter()
.map(|entry| {
let object = entry.as_object().ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world supported action 必须是 JSON object".to_string(),
)
})?;
let action = object
.get("action")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world supported action.action 缺失".to_string(),
)
})?;
let enabled = object
.get("enabled")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| {
SpacetimeClientError::Runtime(
"custom world supported action.enabled 缺失".to_string(),
)
})?;
Ok(CustomWorldSupportedActionRecord {
action: action.to_string(),
enabled,
reason: object
.get("reason")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
})
})
.collect()
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishRuntimeParamsRecord {
pub level_count: u32,
pub merge_count_per_upgrade: u32,
pub spawn_target_count: u32,
pub leader_move_speed: f32,
pub follower_catch_up_speed: f32,
pub offscreen_cull_seconds: f32,
pub prey_spawn_delta_levels: Vec<u32>,
pub threat_spawn_delta_levels: Vec<u32>,
pub win_level: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishGameDraftRecord {
pub title: String,
pub subtitle: String,
pub core_fun: String,
pub ecology_theme: String,
pub levels: Vec<BigFishLevelBlueprintRecord>,
pub background: BigFishBackgroundBlueprintRecord,
pub runtime_params: BigFishRuntimeParamsRecord,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishRuntimeEntityRecord {
pub entity_id: String,
pub level: u32,
pub position: BigFishVector2Record,
pub radius: f32,
pub offscreen_seconds: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BigFishRuntimeRunRecord {
pub run_id: String,
pub session_id: String,
pub status: String,
pub tick: u64,
pub player_level: u32,
pub win_level: u32,
pub leader_entity_id: Option<String>,
pub owned_entities: Vec<BigFishRuntimeEntityRecord>,
pub wild_entities: Vec<BigFishRuntimeEntityRecord>,
pub camera_center: BigFishVector2Record,
pub last_input: BigFishVector2Record,
pub event_log: Vec<String>,
pub updated_at: String,
}