Add user played work stats for puzzle and big fish #2
@@ -17,6 +17,19 @@
|
|||||||
4. 入口必须在移动端单手可点,不遮挡舞台主体。
|
4. 入口必须在移动端单手可点,不遮挡舞台主体。
|
||||||
5. 规则内容只做说明,不参与任何前端裁决;真实规则仍以后端运行快照为准。
|
5. 规则内容只做说明,不参与任何前端裁决;真实规则仍以后端运行快照为准。
|
||||||
|
|
||||||
|
## 游玩统计规则
|
||||||
|
|
||||||
|
所有作品都需要对自身以及用户做游玩统计。
|
||||||
|
|
||||||
|
大鱼吃小鱼正式运行时必须遵守:
|
||||||
|
|
||||||
|
1. 正式开始游玩已发布作品时,更新作品自身播放统计。
|
||||||
|
2. 已登录用户写入 `profile_played_world`,`world_key = big-fish:{session_id}`。
|
||||||
|
3. `profile_id` 保存大鱼作品号/会话号,`world_type = BIG_FISH`。
|
||||||
|
4. `world_title` 使用玩法草稿标题,`world_subtitle` 优先使用副标题,其次使用核心乐趣。
|
||||||
|
5. `owner_user_id` 使用大鱼作品归属用户 ID。
|
||||||
|
6. 退出或结算上报 `elapsedMs` 后,后端按增量刷新 `profile_dashboard_state.total_play_time_ms` 和明细中的 `last_observed_play_time_ms`。
|
||||||
|
|
||||||
## 落地范围
|
## 落地范围
|
||||||
|
|
||||||
1. `src/components/big-fish-runtime/BigFishRuntimeShell.tsx`
|
1. `src/components/big-fish-runtime/BigFishRuntimeShell.tsx`
|
||||||
|
|||||||
@@ -169,6 +169,24 @@ Node 侧入口位于:
|
|||||||
|
|
||||||
## 4. 本轮边界决议
|
## 4. 本轮边界决议
|
||||||
|
|
||||||
|
### 4.0 统一游玩统计规则
|
||||||
|
|
||||||
|
所有作品都需要对自身以及用户做游玩统计。
|
||||||
|
|
||||||
|
正式游玩开始时,玩法自己的作品真相表必须先更新自身统计;已登录用户还必须同步 upsert `profile_played_world` 明细。用户侧明细不是单纯计数,必须保留可跳转的稳定作品标识:
|
||||||
|
|
||||||
|
1. `world_key`
|
||||||
|
2. `world_type`
|
||||||
|
3. `profile_id`
|
||||||
|
4. `world_title`
|
||||||
|
5. `world_subtitle`
|
||||||
|
6. `owner_user_id`
|
||||||
|
7. `first_played_at`
|
||||||
|
8. `last_played_at`
|
||||||
|
9. `last_observed_play_time_ms`
|
||||||
|
|
||||||
|
当玩法有可观测时长时,后端按增量刷新 `profile_dashboard_state.total_play_time_ms`,并同步推进对应 `profile_played_world.last_observed_play_time_ms`。
|
||||||
|
|
||||||
### 4.1 先做 projection 读链
|
### 4.1 先做 projection 读链
|
||||||
|
|
||||||
本轮 profile 三接口只做:
|
本轮 profile 三接口只做:
|
||||||
|
|||||||
@@ -39,6 +39,15 @@
|
|||||||
|
|
||||||
新增拼图成绩表,按“关卡作品 + 网格规格 + 用户”维护最佳成绩。
|
新增拼图成绩表,按“关卡作品 + 网格规格 + 用户”维护最佳成绩。
|
||||||
|
|
||||||
|
正式开始拼图关卡时还必须同步用户玩过作品明细:
|
||||||
|
|
||||||
|
1. 作品自身统计继续更新 `puzzle_work_profile.play_count`。
|
||||||
|
2. 已登录用户写入 `profile_played_world`,`world_key = puzzle:{profile_id}`。
|
||||||
|
3. `profile_id` 保存拼图作品号,`world_type = PUZZLE`。
|
||||||
|
4. `world_title` 使用关卡名,`world_subtitle` 使用作品摘要,`owner_user_id` 使用拼图作者用户 ID。
|
||||||
|
5. 下一关切换到新 `profile_id` 时按同一规则再次写入。
|
||||||
|
6. 排行榜提交携带的 `elapsedMs` 是本关可观测时长,后端按增量累计到 `profile_dashboard_state.total_play_time_ms`。
|
||||||
|
|
||||||
建议字段:
|
建议字段:
|
||||||
|
|
||||||
1. `entry_id`
|
1. `entry_id`
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export type ExecuteBigFishActionRequest = {
|
|||||||
motionKey?: 'idle_float' | 'move_swim' | string;
|
motionKey?: 'idle_float' | 'move_swim' | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RecordBigFishPlayRequest = {
|
||||||
|
elapsedMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SubmitBigFishInputRequest = {
|
export type SubmitBigFishInputRequest = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ use crate::{
|
|||||||
auth_sessions::auth_sessions,
|
auth_sessions::auth_sessions,
|
||||||
big_fish::{
|
big_fish::{
|
||||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
|
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
|
||||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, stream_big_fish_message,
|
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, record_big_fish_play,
|
||||||
record_big_fish_play, submit_big_fish_message,
|
stream_big_fish_message, submit_big_fish_message,
|
||||||
},
|
},
|
||||||
character_animation_assets::{
|
character_animation_assets::{
|
||||||
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
||||||
@@ -589,9 +589,19 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
require_bearer_auth,
|
require_bearer_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/runtime/big-fish/sessions/{session_id}/play",
|
||||||
|
post(record_big_fish_play).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/big-fish/works/{session_id}/play",
|
"/api/runtime/big-fish/works/{session_id}/play",
|
||||||
post(record_big_fish_play),
|
post(record_big_fish_play).route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_bearer_auth,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/runtime/puzzle/agent/sessions",
|
"/api/runtime/puzzle/agent/sessions",
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ use shared_contracts::big_fish::{
|
|||||||
BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse,
|
BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse,
|
||||||
BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse,
|
BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse,
|
||||||
BigFishRuntimeParamsResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
BigFishRuntimeParamsResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
||||||
CreateBigFishSessionRequest, ExecuteBigFishActionRequest, SendBigFishMessageRequest,
|
CreateBigFishSessionRequest, ExecuteBigFishActionRequest, RecordBigFishPlayRequest,
|
||||||
|
SendBigFishMessageRequest,
|
||||||
};
|
};
|
||||||
use shared_contracts::big_fish_works::{BigFishWorkSummaryResponse, BigFishWorksResponse};
|
use shared_contracts::big_fish_works::{BigFishWorkSummaryResponse, BigFishWorksResponse};
|
||||||
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
|
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
|
||||||
@@ -32,8 +33,9 @@ use spacetime_client::{
|
|||||||
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
|
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
|
||||||
BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord,
|
BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord,
|
||||||
BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord, BigFishLevelBlueprintRecord,
|
BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord, BigFishLevelBlueprintRecord,
|
||||||
BigFishMessageSubmitRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
|
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord,
|
||||||
BigFishSessionRecord, BigFishWorkSummaryRecord, SpacetimeClientError,
|
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishWorkSummaryRecord,
|
||||||
|
SpacetimeClientError,
|
||||||
};
|
};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
@@ -195,12 +197,28 @@ pub async fn record_big_fish_play(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(session_id): Path<String>,
|
Path(session_id): Path<String>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||||
|
payload: Result<Json<RecordBigFishPlayRequest>, JsonRejection>,
|
||||||
) -> Result<Json<Value>, Response> {
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let Json(payload) = payload.map_err(|error| {
|
||||||
|
big_fish_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "big-fish",
|
||||||
|
"message": error.body_text(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
||||||
|
|
||||||
let items = state
|
let items = state
|
||||||
.spacetime_client()
|
.spacetime_client()
|
||||||
.record_big_fish_play(session_id, current_utc_micros())
|
.record_big_fish_play(BigFishPlayReportRecordInput {
|
||||||
|
session_id,
|
||||||
|
user_id: authenticated.claims().user_id().to_string(),
|
||||||
|
elapsed_ms: payload.elapsed_ms.unwrap_or(0),
|
||||||
|
reported_at_micros: current_utc_micros(),
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||||
|
|||||||
@@ -321,6 +321,8 @@ pub struct BigFishPublishInput {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct BigFishPlayRecordInput {
|
pub struct BigFishPlayRecordInput {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
pub played_at_micros: i64,
|
pub played_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +668,9 @@ pub fn validate_play_record_input(input: &BigFishPlayRecordInput) -> Result<(),
|
|||||||
if normalize_required_string(&input.session_id).is_none() {
|
if normalize_required_string(&input.session_id).is_none() {
|
||||||
return Err(BigFishFieldError::MissingSessionId);
|
return Err(BigFishFieldError::MissingSessionId);
|
||||||
}
|
}
|
||||||
|
if normalize_required_string(&input.user_id).is_none() {
|
||||||
|
return Err(BigFishFieldError::MissingOwnerUserId);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ pub struct ExecuteBigFishActionRequest {
|
|||||||
pub motion_key: Option<String>,
|
pub motion_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RecordBigFishPlayRequest {
|
||||||
|
#[serde(default)]
|
||||||
|
pub elapsed_ms: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BigFishAnchorItemResponse {
|
pub struct BigFishAnchorItemResponse {
|
||||||
@@ -189,4 +196,14 @@ mod tests {
|
|||||||
assert_eq!(payload["motionKey"], json!("move_swim"));
|
assert_eq!(payload["motionKey"], json!("move_swim"));
|
||||||
assert_eq!(payload["level"], json!(3));
|
assert_eq!(payload["level"], json!(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_big_fish_play_request_uses_camel_case() {
|
||||||
|
let payload = serde_json::to_value(RecordBigFishPlayRequest {
|
||||||
|
elapsed_ms: Some(12_345),
|
||||||
|
})
|
||||||
|
.expect("payload should serialize");
|
||||||
|
|
||||||
|
assert_eq!(payload, json!({ "elapsedMs": 12_345 }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,29 +132,6 @@ impl SpacetimeClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn record_big_fish_play(
|
|
||||||
&self,
|
|
||||||
session_id: String,
|
|
||||||
played_at_micros: i64,
|
|
||||||
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
|
||||||
let procedure_input = BigFishPlayRecordInput {
|
|
||||||
session_id,
|
|
||||||
played_at_micros,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.call_after_connect(move |connection, sender| {
|
|
||||||
connection
|
|
||||||
.procedures()
|
|
||||||
.record_big_fish_play_then(procedure_input, move |_, result| {
|
|
||||||
let mapped = result
|
|
||||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
|
||||||
.and_then(|result| map_big_fish_works_procedure_result(result, None));
|
|
||||||
send_once(&sender, mapped);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn submit_big_fish_message(
|
pub async fn submit_big_fish_message(
|
||||||
&self,
|
&self,
|
||||||
input: BigFishMessageSubmitRecordInput,
|
input: BigFishMessageSubmitRecordInput,
|
||||||
@@ -290,4 +267,28 @@ impl SpacetimeClient {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn record_big_fish_play(
|
||||||
|
&self,
|
||||||
|
input: BigFishPlayReportRecordInput,
|
||||||
|
) -> Result<Vec<BigFishWorkSummaryRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input = BigFishPlayRecordInput {
|
||||||
|
session_id: input.session_id,
|
||||||
|
user_id: input.user_id,
|
||||||
|
elapsed_ms: input.elapsed_ms,
|
||||||
|
played_at_micros: input.reported_at_micros,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.record_big_fish_play_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||||
|
.and_then(|result| map_big_fish_works_procedure_result(result, None));
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ pub use mapper::{
|
|||||||
BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput,
|
BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput,
|
||||||
BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord,
|
BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord,
|
||||||
BigFishLevelBlueprintRecord, BigFishMessageFinalizeRecordInput,
|
BigFishLevelBlueprintRecord, BigFishMessageFinalizeRecordInput,
|
||||||
BigFishMessageSubmitRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
|
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRuntimeParamsRecord,
|
||||||
BigFishSessionRecord, BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord,
|
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishWorkSummaryRecord,
|
||||||
CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord,
|
CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput,
|
||||||
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
|
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput,
|
||||||
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput,
|
CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput,
|
||||||
CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput,
|
CustomWorldAgentOperationProgressRecordInput, CustomWorldAgentOperationRecord,
|
||||||
CustomWorldAgentSessionRecord, CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord,
|
CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentSessionRecord,
|
||||||
|
CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord,
|
||||||
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
|
CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord,
|
||||||
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord,
|
CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord,
|
||||||
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
|
CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord,
|
||||||
|
|||||||
@@ -4364,6 +4364,14 @@ pub struct PuzzleRunNextLevelRecordInput {
|
|||||||
pub advanced_at_micros: i64,
|
pub advanced_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct BigFishPlayReportRecordInput {
|
||||||
|
pub session_id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
|
pub reported_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct PuzzleAnchorItemRecord {
|
pub struct PuzzleAnchorItemRecord {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ impl __sdk::InModule for AdminDisableProfileRedeemCodeArgs {
|
|||||||
type Module = super::RemoteModule;
|
type Module = super::RemoteModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_disable_profile_redeem_code`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
pub trait admin_disable_profile_redeem_code {
|
pub trait admin_disable_profile_redeem_code {
|
||||||
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput) {
|
fn admin_disable_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminDisableInput) {
|
||||||
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
|
self.admin_disable_profile_redeem_code_then(input, |_, _| {});
|
||||||
@@ -25,11 +29,12 @@ pub trait admin_disable_profile_redeem_code {
|
|||||||
fn admin_disable_profile_redeem_code_then(
|
fn admin_disable_profile_redeem_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,17 +42,18 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures {
|
|||||||
fn admin_disable_profile_redeem_code_then(
|
fn admin_disable_profile_redeem_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
) {
|
) {
|
||||||
self.imp
|
self.imp
|
||||||
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
||||||
"admin_disable_profile_redeem_code",
|
"admin_disable_profile_redeem_code",
|
||||||
AdminDisableProfileRedeemCodeArgs { input },
|
AdminDisableProfileRedeemCodeArgs { input },
|
||||||
callback,
|
__callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ impl __sdk::InModule for AdminUpsertProfileRedeemCodeArgs {
|
|||||||
type Module = super::RemoteModule;
|
type Module = super::RemoteModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_upsert_profile_redeem_code`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
pub trait admin_upsert_profile_redeem_code {
|
pub trait admin_upsert_profile_redeem_code {
|
||||||
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput) {
|
fn admin_upsert_profile_redeem_code(&self, input: RuntimeProfileRedeemCodeAdminUpsertInput) {
|
||||||
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
|
self.admin_upsert_profile_redeem_code_then(input, |_, _| {});
|
||||||
@@ -25,11 +29,12 @@ pub trait admin_upsert_profile_redeem_code {
|
|||||||
fn admin_upsert_profile_redeem_code_then(
|
fn admin_upsert_profile_redeem_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,17 +42,18 @@ impl admin_upsert_profile_redeem_code for super::RemoteProcedures {
|
|||||||
fn admin_upsert_profile_redeem_code_then(
|
fn admin_upsert_profile_redeem_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRedeemCodeAdminProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
) {
|
) {
|
||||||
self.imp
|
self.imp
|
||||||
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
.invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>(
|
||||||
"admin_upsert_profile_redeem_code",
|
"admin_upsert_profile_redeem_code",
|
||||||
AdminUpsertProfileRedeemCodeArgs { input },
|
AdminUpsertProfileRedeemCodeArgs { input },
|
||||||
callback,
|
__callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
|||||||
#[sats(crate = __lib)]
|
#[sats(crate = __lib)]
|
||||||
pub struct BigFishPlayRecordInput {
|
pub struct BigFishPlayRecordInput {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
pub played_at_micros: i64,
|
pub played_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
|||||||
|
|
||||||
pub mod accept_quest_reducer;
|
pub mod accept_quest_reducer;
|
||||||
pub mod acknowledge_quest_completion_reducer;
|
pub mod acknowledge_quest_completion_reducer;
|
||||||
|
pub mod admin_disable_profile_redeem_code_procedure;
|
||||||
|
pub mod admin_upsert_profile_redeem_code_procedure;
|
||||||
pub mod advance_puzzle_next_level_procedure;
|
pub mod advance_puzzle_next_level_procedure;
|
||||||
pub mod ai_result_reference_input_type;
|
pub mod ai_result_reference_input_type;
|
||||||
pub mod ai_result_reference_kind_type;
|
pub mod ai_result_reference_kind_type;
|
||||||
@@ -251,9 +253,6 @@ pub mod list_custom_world_works_procedure;
|
|||||||
pub mod list_platform_browse_history_procedure;
|
pub mod list_platform_browse_history_procedure;
|
||||||
pub mod list_profile_save_archives_procedure;
|
pub mod list_profile_save_archives_procedure;
|
||||||
pub mod list_profile_wallet_ledger_procedure;
|
pub mod list_profile_wallet_ledger_procedure;
|
||||||
pub mod redeem_profile_reward_code_procedure;
|
|
||||||
pub mod admin_upsert_profile_redeem_code_procedure;
|
|
||||||
pub mod admin_disable_profile_redeem_code_procedure;
|
|
||||||
pub mod list_puzzle_gallery_procedure;
|
pub mod list_puzzle_gallery_procedure;
|
||||||
pub mod list_puzzle_works_procedure;
|
pub mod list_puzzle_works_procedure;
|
||||||
pub mod npc_battle_interaction_procedure_result_type;
|
pub mod npc_battle_interaction_procedure_result_type;
|
||||||
@@ -281,6 +280,8 @@ pub mod profile_invite_code_type;
|
|||||||
pub mod profile_membership_type;
|
pub mod profile_membership_type;
|
||||||
pub mod profile_played_world_type;
|
pub mod profile_played_world_type;
|
||||||
pub mod profile_recharge_order_type;
|
pub mod profile_recharge_order_type;
|
||||||
|
pub mod profile_redeem_code_type;
|
||||||
|
pub mod profile_redeem_code_usage_type;
|
||||||
pub mod profile_referral_relation_type;
|
pub mod profile_referral_relation_type;
|
||||||
pub mod profile_save_archive_type;
|
pub mod profile_save_archive_type;
|
||||||
pub mod profile_wallet_ledger_type;
|
pub mod profile_wallet_ledger_type;
|
||||||
@@ -335,7 +336,6 @@ pub mod quest_objective_snapshot_type;
|
|||||||
pub mod quest_progress_signal_type;
|
pub mod quest_progress_signal_type;
|
||||||
pub mod quest_record_input_type;
|
pub mod quest_record_input_type;
|
||||||
pub mod quest_record_type;
|
pub mod quest_record_type;
|
||||||
pub mod record_big_fish_play_procedure;
|
|
||||||
pub mod quest_reward_equipment_slot_type;
|
pub mod quest_reward_equipment_slot_type;
|
||||||
pub mod quest_reward_intel_type;
|
pub mod quest_reward_intel_type;
|
||||||
pub mod quest_reward_item_rarity_type;
|
pub mod quest_reward_item_rarity_type;
|
||||||
@@ -348,9 +348,11 @@ pub mod quest_status_type;
|
|||||||
pub mod quest_step_snapshot_type;
|
pub mod quest_step_snapshot_type;
|
||||||
pub mod quest_treasure_inspected_signal_type;
|
pub mod quest_treasure_inspected_signal_type;
|
||||||
pub mod quest_turn_in_input_type;
|
pub mod quest_turn_in_input_type;
|
||||||
|
pub mod record_big_fish_play_procedure;
|
||||||
pub mod redeem_profile_referral_invite_code_procedure;
|
pub mod redeem_profile_referral_invite_code_procedure;
|
||||||
pub mod refund_profile_wallet_points_and_return_procedure;
|
pub mod redeem_profile_reward_code_procedure;
|
||||||
pub mod refresh_session_type;
|
pub mod refresh_session_type;
|
||||||
|
pub mod refund_profile_wallet_points_and_return_procedure;
|
||||||
pub mod resolve_combat_action_and_return_procedure;
|
pub mod resolve_combat_action_and_return_procedure;
|
||||||
pub mod resolve_combat_action_input_type;
|
pub mod resolve_combat_action_input_type;
|
||||||
pub mod resolve_combat_action_procedure_result_type;
|
pub mod resolve_combat_action_procedure_result_type;
|
||||||
@@ -408,6 +410,14 @@ pub mod runtime_profile_recharge_order_snapshot_type;
|
|||||||
pub mod runtime_profile_recharge_order_status_type;
|
pub mod runtime_profile_recharge_order_status_type;
|
||||||
pub mod runtime_profile_recharge_product_kind_type;
|
pub mod runtime_profile_recharge_product_kind_type;
|
||||||
pub mod runtime_profile_recharge_product_snapshot_type;
|
pub mod runtime_profile_recharge_product_snapshot_type;
|
||||||
|
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
||||||
|
pub mod runtime_profile_redeem_code_admin_procedure_result_type;
|
||||||
|
pub mod runtime_profile_redeem_code_admin_upsert_input_type;
|
||||||
|
pub mod runtime_profile_redeem_code_mode_type;
|
||||||
|
pub mod runtime_profile_redeem_code_snapshot_type;
|
||||||
|
pub mod runtime_profile_reward_code_redeem_input_type;
|
||||||
|
pub mod runtime_profile_reward_code_redeem_procedure_result_type;
|
||||||
|
pub mod runtime_profile_reward_code_redeem_snapshot_type;
|
||||||
pub mod runtime_profile_save_archive_list_input_type;
|
pub mod runtime_profile_save_archive_list_input_type;
|
||||||
pub mod runtime_profile_save_archive_procedure_result_type;
|
pub mod runtime_profile_save_archive_procedure_result_type;
|
||||||
pub mod runtime_profile_save_archive_resume_input_type;
|
pub mod runtime_profile_save_archive_resume_input_type;
|
||||||
@@ -418,14 +428,6 @@ pub mod runtime_profile_wallet_ledger_entry_snapshot_type;
|
|||||||
pub mod runtime_profile_wallet_ledger_list_input_type;
|
pub mod runtime_profile_wallet_ledger_list_input_type;
|
||||||
pub mod runtime_profile_wallet_ledger_procedure_result_type;
|
pub mod runtime_profile_wallet_ledger_procedure_result_type;
|
||||||
pub mod runtime_profile_wallet_ledger_source_type_type;
|
pub mod runtime_profile_wallet_ledger_source_type_type;
|
||||||
pub mod runtime_profile_redeem_code_mode_type;
|
|
||||||
pub mod runtime_profile_reward_code_redeem_input_type;
|
|
||||||
pub mod runtime_profile_reward_code_redeem_snapshot_type;
|
|
||||||
pub mod runtime_profile_reward_code_redeem_procedure_result_type;
|
|
||||||
pub mod runtime_profile_redeem_code_admin_upsert_input_type;
|
|
||||||
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
|
||||||
pub mod runtime_profile_redeem_code_snapshot_type;
|
|
||||||
pub mod runtime_profile_redeem_code_admin_procedure_result_type;
|
|
||||||
pub mod runtime_referral_invite_center_get_input_type;
|
pub mod runtime_referral_invite_center_get_input_type;
|
||||||
pub mod runtime_referral_invite_center_procedure_result_type;
|
pub mod runtime_referral_invite_center_procedure_result_type;
|
||||||
pub mod runtime_referral_invite_center_snapshot_type;
|
pub mod runtime_referral_invite_center_snapshot_type;
|
||||||
@@ -490,6 +492,8 @@ pub mod user_browse_history_type;
|
|||||||
|
|
||||||
pub use accept_quest_reducer::accept_quest;
|
pub use accept_quest_reducer::accept_quest;
|
||||||
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
||||||
|
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
||||||
|
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
||||||
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
||||||
pub use ai_result_reference_input_type::AiResultReferenceInput;
|
pub use ai_result_reference_input_type::AiResultReferenceInput;
|
||||||
pub use ai_result_reference_kind_type::AiResultReferenceKind;
|
pub use ai_result_reference_kind_type::AiResultReferenceKind;
|
||||||
@@ -733,9 +737,6 @@ pub use list_custom_world_works_procedure::list_custom_world_works;
|
|||||||
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
pub use list_platform_browse_history_procedure::list_platform_browse_history;
|
||||||
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
pub use list_profile_save_archives_procedure::list_profile_save_archives;
|
||||||
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger;
|
||||||
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
|
|
||||||
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
|
||||||
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
|
||||||
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
|
pub use list_puzzle_gallery_procedure::list_puzzle_gallery;
|
||||||
pub use list_puzzle_works_procedure::list_puzzle_works;
|
pub use list_puzzle_works_procedure::list_puzzle_works;
|
||||||
pub use npc_battle_interaction_procedure_result_type::NpcBattleInteractionProcedureResult;
|
pub use npc_battle_interaction_procedure_result_type::NpcBattleInteractionProcedureResult;
|
||||||
@@ -763,6 +764,8 @@ pub use profile_invite_code_type::ProfileInviteCode;
|
|||||||
pub use profile_membership_type::ProfileMembership;
|
pub use profile_membership_type::ProfileMembership;
|
||||||
pub use profile_played_world_type::ProfilePlayedWorld;
|
pub use profile_played_world_type::ProfilePlayedWorld;
|
||||||
pub use profile_recharge_order_type::ProfileRechargeOrder;
|
pub use profile_recharge_order_type::ProfileRechargeOrder;
|
||||||
|
pub use profile_redeem_code_type::ProfileRedeemCode;
|
||||||
|
pub use profile_redeem_code_usage_type::ProfileRedeemCodeUsage;
|
||||||
pub use profile_referral_relation_type::ProfileReferralRelation;
|
pub use profile_referral_relation_type::ProfileReferralRelation;
|
||||||
pub use profile_save_archive_type::ProfileSaveArchive;
|
pub use profile_save_archive_type::ProfileSaveArchive;
|
||||||
pub use profile_wallet_ledger_type::ProfileWalletLedger;
|
pub use profile_wallet_ledger_type::ProfileWalletLedger;
|
||||||
@@ -817,7 +820,6 @@ pub use quest_objective_snapshot_type::QuestObjectiveSnapshot;
|
|||||||
pub use quest_progress_signal_type::QuestProgressSignal;
|
pub use quest_progress_signal_type::QuestProgressSignal;
|
||||||
pub use quest_record_input_type::QuestRecordInput;
|
pub use quest_record_input_type::QuestRecordInput;
|
||||||
pub use quest_record_type::QuestRecord;
|
pub use quest_record_type::QuestRecord;
|
||||||
pub use record_big_fish_play_procedure::record_big_fish_play;
|
|
||||||
pub use quest_reward_equipment_slot_type::QuestRewardEquipmentSlot;
|
pub use quest_reward_equipment_slot_type::QuestRewardEquipmentSlot;
|
||||||
pub use quest_reward_intel_type::QuestRewardIntel;
|
pub use quest_reward_intel_type::QuestRewardIntel;
|
||||||
pub use quest_reward_item_rarity_type::QuestRewardItemRarity;
|
pub use quest_reward_item_rarity_type::QuestRewardItemRarity;
|
||||||
@@ -830,9 +832,11 @@ pub use quest_status_type::QuestStatus;
|
|||||||
pub use quest_step_snapshot_type::QuestStepSnapshot;
|
pub use quest_step_snapshot_type::QuestStepSnapshot;
|
||||||
pub use quest_treasure_inspected_signal_type::QuestTreasureInspectedSignal;
|
pub use quest_treasure_inspected_signal_type::QuestTreasureInspectedSignal;
|
||||||
pub use quest_turn_in_input_type::QuestTurnInInput;
|
pub use quest_turn_in_input_type::QuestTurnInInput;
|
||||||
|
pub use record_big_fish_play_procedure::record_big_fish_play;
|
||||||
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
|
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
|
||||||
pub use refund_profile_wallet_points_and_return_procedure::refund_profile_wallet_points_and_return;
|
pub use redeem_profile_reward_code_procedure::redeem_profile_reward_code;
|
||||||
pub use refresh_session_type::RefreshSession;
|
pub use refresh_session_type::RefreshSession;
|
||||||
|
pub use refund_profile_wallet_points_and_return_procedure::refund_profile_wallet_points_and_return;
|
||||||
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
|
||||||
pub use resolve_combat_action_input_type::ResolveCombatActionInput;
|
pub use resolve_combat_action_input_type::ResolveCombatActionInput;
|
||||||
pub use resolve_combat_action_procedure_result_type::ResolveCombatActionProcedureResult;
|
pub use resolve_combat_action_procedure_result_type::ResolveCombatActionProcedureResult;
|
||||||
@@ -890,6 +894,14 @@ pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrde
|
|||||||
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
|
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
|
||||||
pub use runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
pub use runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
pub use runtime_profile_recharge_product_snapshot_type::RuntimeProfileRechargeProductSnapshot;
|
pub use runtime_profile_recharge_product_snapshot_type::RuntimeProfileRechargeProductSnapshot;
|
||||||
|
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||||
|
pub use runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||||
|
pub use runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
|
||||||
|
pub use runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||||
|
pub use runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||||
|
pub use runtime_profile_reward_code_redeem_input_type::RuntimeProfileRewardCodeRedeemInput;
|
||||||
|
pub use runtime_profile_reward_code_redeem_procedure_result_type::RuntimeProfileRewardCodeRedeemProcedureResult;
|
||||||
|
pub use runtime_profile_reward_code_redeem_snapshot_type::RuntimeProfileRewardCodeRedeemSnapshot;
|
||||||
pub use runtime_profile_save_archive_list_input_type::RuntimeProfileSaveArchiveListInput;
|
pub use runtime_profile_save_archive_list_input_type::RuntimeProfileSaveArchiveListInput;
|
||||||
pub use runtime_profile_save_archive_procedure_result_type::RuntimeProfileSaveArchiveProcedureResult;
|
pub use runtime_profile_save_archive_procedure_result_type::RuntimeProfileSaveArchiveProcedureResult;
|
||||||
pub use runtime_profile_save_archive_resume_input_type::RuntimeProfileSaveArchiveResumeInput;
|
pub use runtime_profile_save_archive_resume_input_type::RuntimeProfileSaveArchiveResumeInput;
|
||||||
@@ -900,14 +912,6 @@ pub use runtime_profile_wallet_ledger_entry_snapshot_type::RuntimeProfileWalletL
|
|||||||
pub use runtime_profile_wallet_ledger_list_input_type::RuntimeProfileWalletLedgerListInput;
|
pub use runtime_profile_wallet_ledger_list_input_type::RuntimeProfileWalletLedgerListInput;
|
||||||
pub use runtime_profile_wallet_ledger_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult;
|
pub use runtime_profile_wallet_ledger_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult;
|
||||||
pub use runtime_profile_wallet_ledger_source_type_type::RuntimeProfileWalletLedgerSourceType;
|
pub use runtime_profile_wallet_ledger_source_type_type::RuntimeProfileWalletLedgerSourceType;
|
||||||
pub use runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
|
||||||
pub use runtime_profile_reward_code_redeem_input_type::RuntimeProfileRewardCodeRedeemInput;
|
|
||||||
pub use runtime_profile_reward_code_redeem_snapshot_type::RuntimeProfileRewardCodeRedeemSnapshot;
|
|
||||||
pub use runtime_profile_reward_code_redeem_procedure_result_type::RuntimeProfileRewardCodeRedeemProcedureResult;
|
|
||||||
pub use runtime_profile_redeem_code_admin_upsert_input_type::RuntimeProfileRedeemCodeAdminUpsertInput;
|
|
||||||
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
|
||||||
pub use runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
|
||||||
pub use runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
|
||||||
pub use runtime_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput;
|
pub use runtime_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput;
|
||||||
pub use runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult;
|
pub use runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult;
|
||||||
pub use runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot;
|
pub use runtime_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_redeem_code_mode_type::RuntimeProfileRedeemCodeMode;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct ProfileRedeemCode {
|
||||||
|
pub code: String,
|
||||||
|
pub mode: RuntimeProfileRedeemCodeMode,
|
||||||
|
pub reward_points: u64,
|
||||||
|
pub max_uses: u32,
|
||||||
|
pub global_used_count: u32,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub allowed_user_ids: Vec<String>,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at: __sdk::Timestamp,
|
||||||
|
pub updated_at: __sdk::Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ProfileRedeemCode {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `ProfileRedeemCode`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct ProfileRedeemCodeCols {
|
||||||
|
pub code: __sdk::__query_builder::Col<ProfileRedeemCode, String>,
|
||||||
|
pub mode: __sdk::__query_builder::Col<ProfileRedeemCode, RuntimeProfileRedeemCodeMode>,
|
||||||
|
pub reward_points: __sdk::__query_builder::Col<ProfileRedeemCode, u64>,
|
||||||
|
pub max_uses: __sdk::__query_builder::Col<ProfileRedeemCode, u32>,
|
||||||
|
pub global_used_count: __sdk::__query_builder::Col<ProfileRedeemCode, u32>,
|
||||||
|
pub enabled: __sdk::__query_builder::Col<ProfileRedeemCode, bool>,
|
||||||
|
pub allowed_user_ids: __sdk::__query_builder::Col<ProfileRedeemCode, Vec<String>>,
|
||||||
|
pub created_by: __sdk::__query_builder::Col<ProfileRedeemCode, String>,
|
||||||
|
pub created_at: __sdk::__query_builder::Col<ProfileRedeemCode, __sdk::Timestamp>,
|
||||||
|
pub updated_at: __sdk::__query_builder::Col<ProfileRedeemCode, __sdk::Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for ProfileRedeemCode {
|
||||||
|
type Cols = ProfileRedeemCodeCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
ProfileRedeemCodeCols {
|
||||||
|
code: __sdk::__query_builder::Col::new(table_name, "code"),
|
||||||
|
mode: __sdk::__query_builder::Col::new(table_name, "mode"),
|
||||||
|
reward_points: __sdk::__query_builder::Col::new(table_name, "reward_points"),
|
||||||
|
max_uses: __sdk::__query_builder::Col::new(table_name, "max_uses"),
|
||||||
|
global_used_count: __sdk::__query_builder::Col::new(table_name, "global_used_count"),
|
||||||
|
enabled: __sdk::__query_builder::Col::new(table_name, "enabled"),
|
||||||
|
allowed_user_ids: __sdk::__query_builder::Col::new(table_name, "allowed_user_ids"),
|
||||||
|
created_by: __sdk::__query_builder::Col::new(table_name, "created_by"),
|
||||||
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `ProfileRedeemCode`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct ProfileRedeemCodeIxCols {
|
||||||
|
pub code: __sdk::__query_builder::IxCol<ProfileRedeemCode, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for ProfileRedeemCode {
|
||||||
|
type IxCols = ProfileRedeemCodeIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
ProfileRedeemCodeIxCols {
|
||||||
|
code: __sdk::__query_builder::IxCol::new(table_name, "code"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for ProfileRedeemCode {}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// 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 ProfileRedeemCodeUsage {
|
||||||
|
pub usage_id: String,
|
||||||
|
pub code: String,
|
||||||
|
pub user_id: String,
|
||||||
|
pub amount_granted: u64,
|
||||||
|
pub created_at: __sdk::Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ProfileRedeemCodeUsage {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `ProfileRedeemCodeUsage`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct ProfileRedeemCodeUsageCols {
|
||||||
|
pub usage_id: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||||
|
pub code: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||||
|
pub user_id: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, String>,
|
||||||
|
pub amount_granted: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, u64>,
|
||||||
|
pub created_at: __sdk::__query_builder::Col<ProfileRedeemCodeUsage, __sdk::Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for ProfileRedeemCodeUsage {
|
||||||
|
type Cols = ProfileRedeemCodeUsageCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
ProfileRedeemCodeUsageCols {
|
||||||
|
usage_id: __sdk::__query_builder::Col::new(table_name, "usage_id"),
|
||||||
|
code: __sdk::__query_builder::Col::new(table_name, "code"),
|
||||||
|
user_id: __sdk::__query_builder::Col::new(table_name, "user_id"),
|
||||||
|
amount_granted: __sdk::__query_builder::Col::new(table_name, "amount_granted"),
|
||||||
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `ProfileRedeemCodeUsage`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct ProfileRedeemCodeUsageIxCols {
|
||||||
|
pub code: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||||
|
pub usage_id: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||||
|
pub user_id: __sdk::__query_builder::IxCol<ProfileRedeemCodeUsage, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for ProfileRedeemCodeUsage {
|
||||||
|
type IxCols = ProfileRedeemCodeUsageIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
ProfileRedeemCodeUsageIxCols {
|
||||||
|
code: __sdk::__query_builder::IxCol::new(table_name, "code"),
|
||||||
|
usage_id: __sdk::__query_builder::IxCol::new(table_name, "usage_id"),
|
||||||
|
user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for ProfileRedeemCodeUsage {}
|
||||||
@@ -17,6 +17,10 @@ impl __sdk::InModule for RedeemProfileRewardCodeArgs {
|
|||||||
type Module = super::RemoteModule;
|
type Module = super::RemoteModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `redeem_profile_reward_code`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
pub trait redeem_profile_reward_code {
|
pub trait redeem_profile_reward_code {
|
||||||
fn redeem_profile_reward_code(&self, input: RuntimeProfileRewardCodeRedeemInput) {
|
fn redeem_profile_reward_code(&self, input: RuntimeProfileRewardCodeRedeemInput) {
|
||||||
self.redeem_profile_reward_code_then(input, |_, _| {});
|
self.redeem_profile_reward_code_then(input, |_, _| {});
|
||||||
@@ -25,11 +29,12 @@ pub trait redeem_profile_reward_code {
|
|||||||
fn redeem_profile_reward_code_then(
|
fn redeem_profile_reward_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRewardCodeRedeemInput,
|
input: RuntimeProfileRewardCodeRedeemInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,17 +42,18 @@ impl redeem_profile_reward_code for super::RemoteProcedures {
|
|||||||
fn redeem_profile_reward_code_then(
|
fn redeem_profile_reward_code_then(
|
||||||
&self,
|
&self,
|
||||||
input: RuntimeProfileRewardCodeRedeemInput,
|
input: RuntimeProfileRewardCodeRedeemInput,
|
||||||
callback: impl FnOnce(
|
|
||||||
&super::ProcedureEventContext,
|
__callback: impl FnOnce(
|
||||||
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
&super::ProcedureEventContext,
|
||||||
) + Send
|
Result<RuntimeProfileRewardCodeRedeemProcedureResult, __sdk::InternalError>,
|
||||||
+ 'static,
|
) + Send
|
||||||
|
+ 'static,
|
||||||
) {
|
) {
|
||||||
self.imp
|
self.imp
|
||||||
.invoke_procedure_with_callback::<_, RuntimeProfileRewardCodeRedeemProcedureResult>(
|
.invoke_procedure_with_callback::<_, RuntimeProfileRewardCodeRedeemProcedureResult>(
|
||||||
"redeem_profile_reward_code",
|
"redeem_profile_reward_code",
|
||||||
RedeemProfileRewardCodeArgs { input },
|
RedeemProfileRewardCodeArgs { input },
|
||||||
callback,
|
__callback,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use crate::big_fish::tables::{big_fish_agent_message, big_fish_creation_session};
|
use crate::big_fish::tables::{big_fish_agent_message, big_fish_creation_session};
|
||||||
|
use crate::runtime::{
|
||||||
|
ProfilePlayedWorkUpsertInput, add_profile_observed_play_time, upsert_profile_played_work,
|
||||||
|
};
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0;
|
const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0;
|
||||||
@@ -575,6 +578,92 @@ pub(crate) fn compile_big_fish_draft_tx(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn record_big_fish_play_tx(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: BigFishPlayRecordInput,
|
||||||
|
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
||||||
|
validate_play_record_input(&input).map_err(|error| error.to_string())?;
|
||||||
|
let session = ctx
|
||||||
|
.db
|
||||||
|
.big_fish_creation_session()
|
||||||
|
.session_id()
|
||||||
|
.find(&input.session_id)
|
||||||
|
.filter(|row| row.stage == BigFishCreationStage::Published)
|
||||||
|
.ok_or_else(|| "big_fish 已发布作品不存在".to_string())?;
|
||||||
|
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||||
|
let draft = session
|
||||||
|
.draft_json
|
||||||
|
.as_deref()
|
||||||
|
.map(deserialize_draft)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
|
||||||
|
let title = draft
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.title.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.unwrap_or_else(|| "大鱼吃小鱼".to_string());
|
||||||
|
let subtitle = draft
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| {
|
||||||
|
let subtitle = value.subtitle.trim();
|
||||||
|
if subtitle.is_empty() {
|
||||||
|
let core_fun = value.core_fun.trim();
|
||||||
|
(!core_fun.is_empty()).then(|| core_fun.to_string())
|
||||||
|
} else {
|
||||||
|
Some(subtitle.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let world_key = format!("big-fish:{}", session.session_id);
|
||||||
|
|
||||||
|
upsert_profile_played_work(
|
||||||
|
ctx,
|
||||||
|
ProfilePlayedWorkUpsertInput {
|
||||||
|
user_id: input.user_id.clone(),
|
||||||
|
world_key: world_key.clone(),
|
||||||
|
owner_user_id: Some(session.owner_user_id.clone()),
|
||||||
|
profile_id: Some(session.session_id.clone()),
|
||||||
|
world_type: Some("BIG_FISH".to_string()),
|
||||||
|
world_title: title,
|
||||||
|
world_subtitle: subtitle,
|
||||||
|
played_at_micros: input.played_at_micros,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
add_profile_observed_play_time(
|
||||||
|
ctx,
|
||||||
|
&input.user_id,
|
||||||
|
&world_key,
|
||||||
|
input.elapsed_ms,
|
||||||
|
input.played_at_micros,
|
||||||
|
)?;
|
||||||
|
let next_session = BigFishCreationSession {
|
||||||
|
session_id: session.session_id.clone(),
|
||||||
|
owner_user_id: session.owner_user_id.clone(),
|
||||||
|
seed_text: session.seed_text.clone(),
|
||||||
|
current_turn: session.current_turn,
|
||||||
|
progress_percent: session.progress_percent,
|
||||||
|
stage: session.stage,
|
||||||
|
anchor_pack_json: session.anchor_pack_json.clone(),
|
||||||
|
draft_json: session.draft_json.clone(),
|
||||||
|
asset_coverage_json: session.asset_coverage_json.clone(),
|
||||||
|
last_assistant_reply: session.last_assistant_reply.clone(),
|
||||||
|
publish_ready: session.publish_ready,
|
||||||
|
// 中文注释:正式进入已发布作品时同时累加作品播放数,用户侧去重由 profile_played_world 保证。
|
||||||
|
play_count: session.play_count.saturating_add(1),
|
||||||
|
created_at: session.created_at,
|
||||||
|
updated_at: played_at,
|
||||||
|
};
|
||||||
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
|
|
||||||
|
list_big_fish_works_tx(
|
||||||
|
ctx,
|
||||||
|
BigFishWorksListInput {
|
||||||
|
owner_user_id: String::new(),
|
||||||
|
published_only: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_big_fish_session_snapshot(
|
pub(crate) fn build_big_fish_session_snapshot(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
row: &BigFishCreationSession,
|
row: &BigFishCreationSession,
|
||||||
@@ -692,47 +781,6 @@ pub(crate) fn build_big_fish_work_summary(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn record_big_fish_play_tx(
|
|
||||||
ctx: &ReducerContext,
|
|
||||||
input: BigFishPlayRecordInput,
|
|
||||||
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
|
||||||
validate_play_record_input(&input).map_err(|error| error.to_string())?;
|
|
||||||
let session = ctx
|
|
||||||
.db
|
|
||||||
.big_fish_creation_session()
|
|
||||||
.session_id()
|
|
||||||
.find(&input.session_id)
|
|
||||||
.filter(|row| row.stage == BigFishCreationStage::Published)
|
|
||||||
.ok_or_else(|| "big_fish 已发布作品不存在".to_string())?;
|
|
||||||
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
|
||||||
let next_session = BigFishCreationSession {
|
|
||||||
session_id: session.session_id.clone(),
|
|
||||||
owner_user_id: session.owner_user_id.clone(),
|
|
||||||
seed_text: session.seed_text.clone(),
|
|
||||||
current_turn: session.current_turn,
|
|
||||||
progress_percent: session.progress_percent,
|
|
||||||
stage: session.stage,
|
|
||||||
anchor_pack_json: session.anchor_pack_json.clone(),
|
|
||||||
draft_json: session.draft_json.clone(),
|
|
||||||
asset_coverage_json: session.asset_coverage_json.clone(),
|
|
||||||
last_assistant_reply: session.last_assistant_reply.clone(),
|
|
||||||
publish_ready: session.publish_ready,
|
|
||||||
// 中文注释:这里只记录正式发布作品的进入次数,创作结果页测试运行不走这个 procedure。
|
|
||||||
play_count: session.play_count.saturating_add(1),
|
|
||||||
created_at: session.created_at,
|
|
||||||
updated_at: played_at,
|
|
||||||
};
|
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
|
||||||
|
|
||||||
list_big_fish_works_tx(
|
|
||||||
ctx,
|
|
||||||
BigFishWorksListInput {
|
|
||||||
owner_user_id: String::new(),
|
|
||||||
published_only: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn replace_big_fish_session(
|
pub(crate) fn replace_big_fish_session(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
current: &BigFishCreationSession,
|
current: &BigFishCreationSession,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
use crate::runtime::{
|
||||||
|
ProfilePlayedWorkUpsertInput, add_profile_observed_play_time, upsert_profile_played_work,
|
||||||
|
};
|
||||||
use module_puzzle::{
|
use module_puzzle::{
|
||||||
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
||||||
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
|
PuzzleAgentMessageRole, PuzzleAgentMessageSnapshot, PuzzleAgentSessionCreateInput,
|
||||||
@@ -1072,6 +1075,12 @@ fn start_puzzle_run_tx(
|
|||||||
.map(|value| value.profile_id.clone());
|
.map(|value| value.profile_id.clone());
|
||||||
|
|
||||||
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
||||||
|
upsert_puzzle_profile_played_work(
|
||||||
|
ctx,
|
||||||
|
&input.owner_user_id,
|
||||||
|
&entry_profile_row,
|
||||||
|
input.started_at_micros,
|
||||||
|
)?;
|
||||||
insert_puzzle_runtime_run(ctx, &run, &input.owner_user_id, input.started_at_micros)?;
|
insert_puzzle_runtime_run(ctx, &run, &input.owner_user_id, input.started_at_micros)?;
|
||||||
Ok(run)
|
Ok(run)
|
||||||
}
|
}
|
||||||
@@ -1179,6 +1188,12 @@ fn advance_puzzle_next_level_tx(
|
|||||||
.find(&next_profile.profile_id)
|
.find(&next_profile.profile_id)
|
||||||
{
|
{
|
||||||
increment_puzzle_profile_play_count(ctx, &next_profile_row, input.advanced_at_micros);
|
increment_puzzle_profile_play_count(ctx, &next_profile_row, input.advanced_at_micros);
|
||||||
|
upsert_puzzle_profile_played_work(
|
||||||
|
ctx,
|
||||||
|
&input.owner_user_id,
|
||||||
|
&next_profile_row,
|
||||||
|
input.advanced_at_micros,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.advanced_at_micros);
|
replace_puzzle_runtime_run(ctx, &row, &next_run, input.advanced_at_micros);
|
||||||
Ok(next_run)
|
Ok(next_run)
|
||||||
@@ -1219,6 +1234,13 @@ fn submit_puzzle_leaderboard_entry_tx(
|
|||||||
&input.run_id,
|
&input.run_id,
|
||||||
input.submitted_at_micros,
|
input.submitted_at_micros,
|
||||||
);
|
);
|
||||||
|
add_profile_observed_play_time(
|
||||||
|
ctx,
|
||||||
|
&input.owner_user_id,
|
||||||
|
&format!("puzzle:{}", input.profile_id),
|
||||||
|
input.elapsed_ms.max(1_000),
|
||||||
|
input.submitted_at_micros,
|
||||||
|
)?;
|
||||||
|
|
||||||
let leaderboard_entries = list_puzzle_leaderboard_entries(
|
let leaderboard_entries = list_puzzle_leaderboard_entries(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1607,6 +1629,28 @@ fn increment_puzzle_profile_play_count(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_puzzle_profile_played_work(
|
||||||
|
ctx: &TxContext,
|
||||||
|
user_id: &str,
|
||||||
|
row: &PuzzleWorkProfileRow,
|
||||||
|
played_at_micros: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// 拼图正式游玩以作品 profile_id 作为公开作品号,用户侧明细按 world_key 去重。
|
||||||
|
upsert_profile_played_work(
|
||||||
|
ctx,
|
||||||
|
ProfilePlayedWorkUpsertInput {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
world_key: format!("puzzle:{}", row.profile_id),
|
||||||
|
owner_user_id: Some(row.owner_user_id.clone()),
|
||||||
|
profile_id: Some(row.profile_id.clone()),
|
||||||
|
world_type: Some("PUZZLE".to_string()),
|
||||||
|
world_title: row.level_name.clone(),
|
||||||
|
world_subtitle: row.summary.clone(),
|
||||||
|
played_at_micros,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn replace_generated_candidate(
|
fn replace_generated_candidate(
|
||||||
draft: &mut PuzzleResultDraft,
|
draft: &mut PuzzleResultDraft,
|
||||||
candidates: Vec<PuzzleGeneratedImageCandidate>,
|
candidates: Vec<PuzzleGeneratedImageCandidate>,
|
||||||
|
|||||||
@@ -116,6 +116,17 @@ pub struct ProfilePlayedWorld {
|
|||||||
pub(crate) last_observed_play_time_ms: u64,
|
pub(crate) last_observed_play_time_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ProfilePlayedWorkUpsertInput {
|
||||||
|
pub(crate) user_id: String,
|
||||||
|
pub(crate) world_key: String,
|
||||||
|
pub(crate) owner_user_id: Option<String>,
|
||||||
|
pub(crate) profile_id: Option<String>,
|
||||||
|
pub(crate) world_type: Option<String>,
|
||||||
|
pub(crate) world_title: String,
|
||||||
|
pub(crate) world_subtitle: String,
|
||||||
|
pub(crate) played_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(accessor = profile_membership)]
|
#[spacetimedb::table(accessor = profile_membership)]
|
||||||
pub struct ProfileMembership {
|
pub struct ProfileMembership {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
@@ -589,6 +600,172 @@ pub(crate) fn sync_profile_projections_from_snapshot(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upsert_profile_played_work(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: ProfilePlayedWorkUpsertInput,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let user_id = input.user_id.trim();
|
||||||
|
let world_key = input.world_key.trim();
|
||||||
|
if user_id.is_empty() {
|
||||||
|
return Err("profile_played_world.user_id 不能为空".to_string());
|
||||||
|
}
|
||||||
|
if world_key.is_empty() {
|
||||||
|
return Err("profile_played_world.world_key 不能为空".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||||
|
let played_world_id = format!("{user_id}:{world_key}");
|
||||||
|
let existing = ctx
|
||||||
|
.db
|
||||||
|
.profile_played_world()
|
||||||
|
.played_world_id()
|
||||||
|
.find(&played_world_id);
|
||||||
|
|
||||||
|
if let Some(existing) = existing {
|
||||||
|
ctx.db
|
||||||
|
.profile_played_world()
|
||||||
|
.played_world_id()
|
||||||
|
.delete(&existing.played_world_id);
|
||||||
|
ctx.db.profile_played_world().insert(ProfilePlayedWorld {
|
||||||
|
played_world_id,
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
world_key: world_key.to_string(),
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
world_type: input.world_type,
|
||||||
|
world_title: input.world_title,
|
||||||
|
world_subtitle: input.world_subtitle,
|
||||||
|
first_played_at: existing.first_played_at,
|
||||||
|
last_played_at: played_at,
|
||||||
|
last_observed_play_time_ms: existing.last_observed_play_time_ms,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.db.profile_played_world().insert(ProfilePlayedWorld {
|
||||||
|
played_world_id,
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
world_key: world_key.to_string(),
|
||||||
|
owner_user_id: input.owner_user_id,
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
world_type: input.world_type,
|
||||||
|
world_title: input.world_title,
|
||||||
|
world_subtitle: input.world_subtitle,
|
||||||
|
first_played_at: played_at,
|
||||||
|
last_played_at: played_at,
|
||||||
|
last_observed_play_time_ms: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_profile_dashboard_state(ctx, user_id, played_at);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_profile_observed_play_time(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
user_id: &str,
|
||||||
|
world_key: &str,
|
||||||
|
elapsed_ms: u64,
|
||||||
|
observed_at_micros: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let user_id = user_id.trim();
|
||||||
|
let world_key = world_key.trim();
|
||||||
|
if user_id.is_empty() || world_key.is_empty() || elapsed_ms == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let observed_at = Timestamp::from_micros_since_unix_epoch(observed_at_micros);
|
||||||
|
let played_world_id = format!("{user_id}:{world_key}");
|
||||||
|
if let Some(existing) = ctx
|
||||||
|
.db
|
||||||
|
.profile_played_world()
|
||||||
|
.played_world_id()
|
||||||
|
.find(&played_world_id)
|
||||||
|
{
|
||||||
|
ctx.db
|
||||||
|
.profile_played_world()
|
||||||
|
.played_world_id()
|
||||||
|
.delete(&existing.played_world_id);
|
||||||
|
ctx.db.profile_played_world().insert(ProfilePlayedWorld {
|
||||||
|
played_world_id,
|
||||||
|
user_id: existing.user_id,
|
||||||
|
world_key: existing.world_key,
|
||||||
|
owner_user_id: existing.owner_user_id,
|
||||||
|
profile_id: existing.profile_id,
|
||||||
|
world_type: existing.world_type,
|
||||||
|
world_title: existing.world_title,
|
||||||
|
world_subtitle: existing.world_subtitle,
|
||||||
|
first_played_at: existing.first_played_at,
|
||||||
|
last_played_at: observed_at,
|
||||||
|
last_observed_play_time_ms: existing
|
||||||
|
.last_observed_play_time_ms
|
||||||
|
.saturating_add(elapsed_ms),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_profile_dashboard_play_time(ctx, user_id, elapsed_ms, observed_at);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_profile_dashboard_state(ctx: &ReducerContext, user_id: &str, updated_at: Timestamp) {
|
||||||
|
if ctx
|
||||||
|
.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.user_id()
|
||||||
|
.find(&user_id.to_string())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.insert(ProfileDashboardState {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
wallet_balance: 0,
|
||||||
|
total_play_time_ms: 0,
|
||||||
|
created_at: updated_at,
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_profile_dashboard_play_time(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
user_id: &str,
|
||||||
|
elapsed_ms: u64,
|
||||||
|
updated_at: Timestamp,
|
||||||
|
) {
|
||||||
|
let current = ctx
|
||||||
|
.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.user_id()
|
||||||
|
.find(&user_id.to_string());
|
||||||
|
|
||||||
|
if let Some(existing) = current {
|
||||||
|
ctx.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.user_id()
|
||||||
|
.delete(&existing.user_id);
|
||||||
|
ctx.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.insert(ProfileDashboardState {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
wallet_balance: existing.wallet_balance,
|
||||||
|
total_play_time_ms: existing.total_play_time_ms.saturating_add(elapsed_ms),
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.db
|
||||||
|
.profile_dashboard_state()
|
||||||
|
.insert(ProfileDashboardState {
|
||||||
|
user_id: user_id.to_string(),
|
||||||
|
wallet_balance: 0,
|
||||||
|
total_play_time_ms: elapsed_ms,
|
||||||
|
created_at: updated_at,
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_profile_dashboard_from_snapshot(
|
fn sync_profile_dashboard_from_snapshot(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
snapshot: &RuntimeSnapshot,
|
snapshot: &RuntimeSnapshot,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/p
|
|||||||
import type {
|
import type {
|
||||||
CustomWorldGalleryCard,
|
CustomWorldGalleryCard,
|
||||||
CustomWorldLibraryEntry,
|
CustomWorldLibraryEntry,
|
||||||
|
ProfilePlayedWorkSummary,
|
||||||
|
ProfilePlayStatsResponse,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||||
import {
|
import {
|
||||||
@@ -55,12 +57,12 @@ import {
|
|||||||
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
import { listBigFishGallery } from '../../services/big-fish-gallery';
|
||||||
import {
|
import {
|
||||||
advanceLocalBigFishRuntimeRun,
|
advanceLocalBigFishRuntimeRun,
|
||||||
|
recordBigFishPlay,
|
||||||
startLocalBigFishRuntimeRun,
|
startLocalBigFishRuntimeRun,
|
||||||
} from '../../services/big-fish-runtime';
|
} from '../../services/big-fish-runtime';
|
||||||
import {
|
import {
|
||||||
deleteBigFishWork,
|
deleteBigFishWork,
|
||||||
listBigFishWorks,
|
listBigFishWorks,
|
||||||
recordBigFishWorkPlay,
|
|
||||||
} from '../../services/big-fish-works';
|
} from '../../services/big-fish-works';
|
||||||
import {
|
import {
|
||||||
readCustomWorldAgentUiState,
|
readCustomWorldAgentUiState,
|
||||||
@@ -107,6 +109,7 @@ import {
|
|||||||
deleteRpgEntryWorldProfile,
|
deleteRpgEntryWorldProfile,
|
||||||
getRpgEntryWorldGalleryDetailByCode,
|
getRpgEntryWorldGalleryDetailByCode,
|
||||||
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||||
|
import { getRpgProfilePlayStats } from '../../services/rpg-entry/rpgProfileClient';
|
||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
import { useAuthUi } from '../auth/AuthUiContext';
|
import { useAuthUi } from '../auth/AuthUiContext';
|
||||||
import {
|
import {
|
||||||
@@ -154,6 +157,8 @@ type AgentResultBlockerView = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BigFishRuntimeSessionSource = 'draft' | 'work' | null;
|
||||||
|
|
||||||
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
||||||
'publish_missing_world_hook',
|
'publish_missing_world_hook',
|
||||||
'publish_missing_player_premise',
|
'publish_missing_player_premise',
|
||||||
@@ -442,6 +447,13 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
title: string;
|
title: string;
|
||||||
publicWorkCode: string;
|
publicWorkCode: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [bigFishRuntimeWork, setBigFishRuntimeWork] =
|
||||||
|
useState<BigFishWorkSummary | null>(null);
|
||||||
|
const [bigFishRuntimeStartedAt, setBigFishRuntimeStartedAt] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
const [bigFishRuntimeSessionSource, setBigFishRuntimeSessionSource] =
|
||||||
|
useState<BigFishRuntimeSessionSource>(null);
|
||||||
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
|
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
|
||||||
const [bigFishGenerationState, setBigFishGenerationState] =
|
const [bigFishGenerationState, setBigFishGenerationState] =
|
||||||
useState<MiniGameDraftGenerationState | null>(null);
|
useState<MiniGameDraftGenerationState | null>(null);
|
||||||
@@ -474,6 +486,14 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
||||||
string | null
|
string | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [profilePlayStats, setProfilePlayStats] =
|
||||||
|
useState<ProfilePlayStatsResponse | null>(null);
|
||||||
|
const [profilePlayStatsError, setProfilePlayStatsError] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [isProfilePlayStatsLoading, setIsProfilePlayStatsLoading] =
|
||||||
|
useState(false);
|
||||||
|
const [isProfilePlayStatsOpen, setIsProfilePlayStatsOpen] = useState(false);
|
||||||
const hadReadableProtectedDataRef = useRef(false);
|
const hadReadableProtectedDataRef = useRef(false);
|
||||||
const hasInitialAgentSession = Boolean(
|
const hasInitialAgentSession = Boolean(
|
||||||
readCustomWorldAgentUiState().activeSessionId &&
|
readCustomWorldAgentUiState().activeSessionId &&
|
||||||
@@ -986,7 +1006,6 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
const bigFishError = bigFishFlow.error;
|
const bigFishError = bigFishFlow.error;
|
||||||
const setBigFishError = bigFishFlow.setError;
|
const setBigFishError = bigFishFlow.setError;
|
||||||
const isBigFishBusy = bigFishFlow.isBusy;
|
const isBigFishBusy = bigFishFlow.isBusy;
|
||||||
const setIsBigFishBusy = bigFishFlow.setIsBusy;
|
|
||||||
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
|
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
|
||||||
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
|
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
|
||||||
|
|
||||||
@@ -1034,6 +1053,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setBigFishWorks([]);
|
setBigFishWorks([]);
|
||||||
setBigFishRun(null);
|
setBigFishRun(null);
|
||||||
setBigFishRuntimeShare(null);
|
setBigFishRuntimeShare(null);
|
||||||
|
setBigFishRuntimeWork(null);
|
||||||
|
setBigFishRuntimeStartedAt(null);
|
||||||
|
setBigFishRuntimeSessionSource(null);
|
||||||
setBigFishGenerationState(null);
|
setBigFishGenerationState(null);
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
setPuzzleOperation(null);
|
setPuzzleOperation(null);
|
||||||
@@ -1045,6 +1067,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
setIsPuzzleNextLevelGenerating(false);
|
setIsPuzzleNextLevelGenerating(false);
|
||||||
setPuzzleError(null);
|
setPuzzleError(null);
|
||||||
setDeletingCreationWorkId(null);
|
setDeletingCreationWorkId(null);
|
||||||
|
setProfilePlayStats(null);
|
||||||
|
setProfilePlayStatsError(null);
|
||||||
|
setIsProfilePlayStatsOpen(false);
|
||||||
resetRpgSessionViewState();
|
resetRpgSessionViewState();
|
||||||
setRpgGeneratedCustomWorldProfile(null);
|
setRpgGeneratedCustomWorldProfile(null);
|
||||||
setRpgCustomWorldError(null);
|
setRpgCustomWorldError(null);
|
||||||
@@ -1113,6 +1138,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const leaveBigFishFlow = useCallback(() => {
|
const leaveBigFishFlow = useCallback(() => {
|
||||||
setBigFishRun(null);
|
setBigFishRun(null);
|
||||||
|
setBigFishRuntimeWork(null);
|
||||||
|
setBigFishRuntimeStartedAt(null);
|
||||||
|
setBigFishRuntimeSessionSource(null);
|
||||||
setBigFishGenerationState(null);
|
setBigFishGenerationState(null);
|
||||||
bigFishFlow.leaveFlow();
|
bigFishFlow.leaveFlow();
|
||||||
}, [bigFishFlow]);
|
}, [bigFishFlow]);
|
||||||
@@ -1149,32 +1177,62 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = async () => {
|
const sessionId = bigFishSession.sessionId;
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
setBigFishRuntimeShare(null);
|
setBigFishRuntimeShare(null);
|
||||||
if (bigFishSession.stage === 'published') {
|
setBigFishRuntimeWork(null);
|
||||||
await recordBigFishWorkPlay(bigFishSession.sessionId);
|
setBigFishRuntimeStartedAt(Date.now());
|
||||||
await refreshBigFishShelf();
|
setBigFishRuntimeSessionSource('draft');
|
||||||
}
|
setBigFishRun(startLocalBigFishRuntimeRun({ session: bigFishSession }));
|
||||||
setBigFishRun(startLocalBigFishRuntimeRun({ session: bigFishSession }));
|
setSelectionStage('big-fish-runtime');
|
||||||
setSelectionStage('big-fish-runtime');
|
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||||
};
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||||
void run().catch((error) => {
|
);
|
||||||
setBigFishError(resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'));
|
|
||||||
});
|
});
|
||||||
}, [bigFishSession, refreshBigFishShelf, resolveBigFishErrorMessage, setSelectionStage]);
|
void refreshBigFishShelf();
|
||||||
|
}, [
|
||||||
|
bigFishSession,
|
||||||
|
refreshBigFishShelf,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
setSelectionStage,
|
||||||
|
]);
|
||||||
|
|
||||||
const restartBigFishRun = useCallback(() => {
|
const restartBigFishRun = useCallback(() => {
|
||||||
if (!bigFishSession && !bigFishRun) {
|
if (!bigFishSession && !bigFishRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sessionId = bigFishSession?.sessionId ?? bigFishRun?.sessionId;
|
||||||
|
if (!sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
setBigFishRuntimeShare(null);
|
if (bigFishSession) {
|
||||||
setBigFishRun(startLocalBigFishRuntimeRun({ session: bigFishSession }));
|
setBigFishRuntimeShare(null);
|
||||||
|
}
|
||||||
|
setBigFishRuntimeStartedAt(Date.now());
|
||||||
|
setBigFishRuntimeSessionSource(bigFishSession ? 'draft' : 'work');
|
||||||
|
setBigFishRun(
|
||||||
|
startLocalBigFishRuntimeRun({
|
||||||
|
session: bigFishSession,
|
||||||
|
work: bigFishRuntimeWork,
|
||||||
|
}),
|
||||||
|
);
|
||||||
setSelectionStage('big-fish-runtime');
|
setSelectionStage('big-fish-runtime');
|
||||||
}, [bigFishRun, bigFishSession, setSelectionStage]);
|
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
bigFishRun,
|
||||||
|
bigFishRuntimeWork,
|
||||||
|
bigFishSession,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
setSelectionStage,
|
||||||
|
]);
|
||||||
|
|
||||||
const startPuzzleRunFromProfile = useCallback(
|
const startPuzzleRunFromProfile = useCallback(
|
||||||
async (profileId: string) => {
|
async (profileId: string) => {
|
||||||
@@ -1265,12 +1323,33 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBigFishRun((currentRun) =>
|
setBigFishRun((currentRun) =>
|
||||||
currentRun ? advanceLocalBigFishRuntimeRun(currentRun, payload) : currentRun,
|
currentRun
|
||||||
|
? advanceLocalBigFishRuntimeRun(currentRun, payload)
|
||||||
|
: currentRun,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[bigFishRun],
|
[bigFishRun],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reportBigFishObservedPlayTime = useCallback(() => {
|
||||||
|
const sessionId = bigFishRun?.sessionId?.trim();
|
||||||
|
if (!sessionId || !bigFishRuntimeStartedAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedMs = Math.max(1_000, Date.now() - bigFishRuntimeStartedAt);
|
||||||
|
setBigFishRuntimeStartedAt(null);
|
||||||
|
void recordBigFishPlay(sessionId, { elapsedMs }).catch((error) => {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
bigFishRun?.sessionId,
|
||||||
|
bigFishRuntimeStartedAt,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
const swapPuzzlePiecesInRun = useCallback(
|
const swapPuzzlePiecesInRun = useCallback(
|
||||||
(payload: { firstPieceId: string; secondPieceId: string }) => {
|
(payload: { firstPieceId: string; secondPieceId: string }) => {
|
||||||
if (!puzzleRun || isPuzzleBusy) {
|
if (!puzzleRun || isPuzzleBusy) {
|
||||||
@@ -1327,7 +1406,9 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
submittedPuzzleLeaderboardKeysRef.current.delete(submitKey);
|
submittedPuzzleLeaderboardKeysRef.current.delete(submitKey);
|
||||||
setPuzzleError(resolvePuzzleErrorMessage(error, '提交拼图排行榜失败。'));
|
setPuzzleError(
|
||||||
|
resolvePuzzleErrorMessage(error, '提交拼图排行榜失败。'),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsPuzzleLeaderboardBusy(false);
|
setIsPuzzleLeaderboardBusy(false);
|
||||||
@@ -1697,26 +1778,34 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const startBigFishRunFromWork = useCallback(
|
const startBigFishRunFromWork = useCallback(
|
||||||
(item: BigFishWorkSummary) => {
|
(item: BigFishWorkSummary) => {
|
||||||
const sessionId = item.sourceSessionId?.trim();
|
const sessionId = item.sourceSessionId?.trim();
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicWorkCode = buildBigFishPublicWorkCode(item.sourceSessionId);
|
const publicWorkCode = buildBigFishPublicWorkCode(item.sourceSessionId);
|
||||||
setBigFishError(null);
|
setBigFishError(null);
|
||||||
bigFishFlow.setSession(null);
|
bigFishFlow.setSession(null);
|
||||||
setBigFishRuntimeShare({
|
setBigFishRuntimeWork(item);
|
||||||
title: item.title,
|
setBigFishRuntimeShare({
|
||||||
publicWorkCode,
|
title: item.title,
|
||||||
});
|
publicWorkCode,
|
||||||
setBigFishRun(startLocalBigFishRuntimeRun({ work: item }));
|
});
|
||||||
setSelectionStage('big-fish-runtime');
|
setBigFishRuntimeStartedAt(Date.now());
|
||||||
pushAppHistoryPath(
|
setBigFishRuntimeSessionSource('work');
|
||||||
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
|
setBigFishRun(startLocalBigFishRuntimeRun({ work: item }));
|
||||||
);
|
setSelectionStage('big-fish-runtime');
|
||||||
},
|
pushAppHistoryPath(
|
||||||
[bigFishFlow, setSelectionStage],
|
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
|
||||||
|
);
|
||||||
|
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePublicCodeSearch = useCallback(
|
const handlePublicCodeSearch = useCallback(
|
||||||
@@ -1865,6 +1954,118 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openProfilePlayedWorks = useCallback(() => {
|
||||||
|
setIsProfilePlayStatsOpen(true);
|
||||||
|
setIsProfilePlayStatsLoading(true);
|
||||||
|
setProfilePlayStatsError(null);
|
||||||
|
|
||||||
|
void getRpgProfilePlayStats()
|
||||||
|
.then(setProfilePlayStats)
|
||||||
|
.catch((error) => {
|
||||||
|
setProfilePlayStats(null);
|
||||||
|
setProfilePlayStatsError(
|
||||||
|
resolveRpgCreationErrorMessage(error, '读取玩过作品失败。'),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsProfilePlayStatsLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openPlayedWork = useCallback(
|
||||||
|
(work: ProfilePlayedWorkSummary) => {
|
||||||
|
const worldType = (work.worldType ?? '').toLowerCase();
|
||||||
|
setIsProfilePlayStatsOpen(false);
|
||||||
|
|
||||||
|
if (worldType === 'puzzle' || work.worldKey.startsWith('puzzle:')) {
|
||||||
|
const profileId =
|
||||||
|
work.profileId ?? work.worldKey.replace(/^puzzle:/u, '');
|
||||||
|
if (profileId) {
|
||||||
|
void openPuzzleDetail(profileId, { tab: 'profile' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
worldType === 'big_fish' ||
|
||||||
|
worldType === 'big-fish' ||
|
||||||
|
work.worldKey.startsWith('big-fish:')
|
||||||
|
) {
|
||||||
|
const sessionId =
|
||||||
|
work.profileId ?? work.worldKey.replace(/^big-fish:/u, '');
|
||||||
|
if (!sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void refreshBigFishGallery()
|
||||||
|
.then((entries) => {
|
||||||
|
const matchedEntry = entries.find(
|
||||||
|
(entry) => entry.sourceSessionId === sessionId,
|
||||||
|
);
|
||||||
|
if (matchedEntry) {
|
||||||
|
startBigFishRunFromWork(matchedEntry);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startBigFishRunFromWork({
|
||||||
|
workId: `big-fish:${sessionId}`,
|
||||||
|
sourceSessionId: sessionId,
|
||||||
|
ownerUserId: work.ownerUserId ?? '',
|
||||||
|
title: work.worldTitle,
|
||||||
|
subtitle: work.worldSubtitle,
|
||||||
|
summary: work.worldSubtitle,
|
||||||
|
coverImageSrc: null,
|
||||||
|
status: 'published',
|
||||||
|
updatedAt: work.lastPlayedAt,
|
||||||
|
publishReady: true,
|
||||||
|
levelCount: 0,
|
||||||
|
levelMainImageReadyCount: 0,
|
||||||
|
levelMotionReadyCount: 0,
|
||||||
|
backgroundReady: false,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setBigFishError(
|
||||||
|
resolveBigFishErrorMessage(error, '进入大鱼吃小鱼作品失败。'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileId = work.profileId ?? work.worldKey;
|
||||||
|
const ownerUserId = work.ownerUserId;
|
||||||
|
if (!ownerUserId || !profileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runProtectedAction(() => {
|
||||||
|
void detailNavigation.openGalleryDetail({
|
||||||
|
ownerUserId,
|
||||||
|
profileId,
|
||||||
|
publicWorkCode: null,
|
||||||
|
authorPublicUserCode: null,
|
||||||
|
visibility: 'published',
|
||||||
|
publishedAt: work.firstPlayedAt,
|
||||||
|
updatedAt: work.lastPlayedAt,
|
||||||
|
authorDisplayName: work.worldSubtitle,
|
||||||
|
worldName: work.worldTitle,
|
||||||
|
subtitle: work.worldSubtitle,
|
||||||
|
summaryText: '',
|
||||||
|
coverImageSrc: null,
|
||||||
|
themeMode: 'martial',
|
||||||
|
playableNpcCount: 0,
|
||||||
|
landmarkCount: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
detailNavigation,
|
||||||
|
openPuzzleDetail,
|
||||||
|
refreshBigFishGallery,
|
||||||
|
resolveBigFishErrorMessage,
|
||||||
|
runProtectedAction,
|
||||||
|
startBigFishRunFromWork,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const publicWorkCode = initialPublicWorkCode?.trim();
|
const publicWorkCode = initialPublicWorkCode?.trim();
|
||||||
if (
|
if (
|
||||||
@@ -2120,7 +2321,19 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
void handlePublicCodeSearch(keyword);
|
void handlePublicCodeSearch(keyword);
|
||||||
}}
|
}}
|
||||||
isSearchingPublicCode={isSearchingPublicCode}
|
isSearchingPublicCode={isSearchingPublicCode}
|
||||||
onOpenProfileDashboardCard={() => {
|
profilePlayStats={profilePlayStats}
|
||||||
|
isProfilePlayStatsOpen={isProfilePlayStatsOpen}
|
||||||
|
isProfilePlayStatsLoading={isProfilePlayStatsLoading}
|
||||||
|
profilePlayStatsError={profilePlayStatsError}
|
||||||
|
onCloseProfilePlayStats={() => {
|
||||||
|
setIsProfilePlayStatsOpen(false);
|
||||||
|
}}
|
||||||
|
onOpenPlayedWork={openPlayedWork}
|
||||||
|
onOpenProfileDashboardCard={(cardKey) => {
|
||||||
|
if (cardKey === 'playedWorks') {
|
||||||
|
openProfilePlayedWorks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (platformBootstrap.dashboardError) {
|
if (platformBootstrap.dashboardError) {
|
||||||
void platformBootstrap.refreshProfileDashboard();
|
void platformBootstrap.refreshProfileDashboard();
|
||||||
}
|
}
|
||||||
@@ -2373,11 +2586,15 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
isBusy={isBigFishBusy}
|
isBusy={isBigFishBusy}
|
||||||
error={bigFishError}
|
error={bigFishError}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
|
reportBigFishObservedPlayTime();
|
||||||
setSelectionStage(
|
setSelectionStage(
|
||||||
bigFishSession ? 'big-fish-result' : 'platform',
|
bigFishRuntimeSessionSource === 'draft'
|
||||||
|
? 'big-fish-result'
|
||||||
|
: 'platform',
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onRestart={() => {
|
onRestart={() => {
|
||||||
|
reportBigFishObservedPlayTime();
|
||||||
void restartBigFishRun();
|
void restartBigFishRun();
|
||||||
}}
|
}}
|
||||||
onSubmitInput={submitBigFishInput}
|
onSubmitInput={submitBigFishInput}
|
||||||
@@ -2545,17 +2762,17 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
<Suspense
|
<Suspense
|
||||||
fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}
|
fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}
|
||||||
>
|
>
|
||||||
<PuzzleRuntimeShell
|
<PuzzleRuntimeShell
|
||||||
run={puzzleRun}
|
run={puzzleRun}
|
||||||
isBusy={
|
isBusy={
|
||||||
isPuzzleBusy ||
|
isPuzzleBusy ||
|
||||||
isPuzzleNextLevelGenerating ||
|
isPuzzleNextLevelGenerating ||
|
||||||
isPuzzleLeaderboardBusy
|
isPuzzleLeaderboardBusy
|
||||||
}
|
}
|
||||||
error={puzzleError}
|
error={puzzleError}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setSelectionStage(puzzleRuntimeReturnStage);
|
setSelectionStage(puzzleRuntimeReturnStage);
|
||||||
}}
|
}}
|
||||||
onSwapPieces={(payload) => {
|
onSwapPieces={(payload) => {
|
||||||
void swapPuzzlePiecesInRun(payload);
|
void swapPuzzlePiecesInRun(payload);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ import type {
|
|||||||
PlatformBrowseHistoryEntry,
|
PlatformBrowseHistoryEntry,
|
||||||
ProfileDashboardCardKey,
|
ProfileDashboardCardKey,
|
||||||
ProfileDashboardSummary,
|
ProfileDashboardSummary,
|
||||||
ProfileWalletLedgerResponse,
|
ProfilePlayedWorkSummary,
|
||||||
|
ProfilePlayStatsResponse,
|
||||||
ProfileReferralInviteCenterResponse,
|
ProfileReferralInviteCenterResponse,
|
||||||
ProfileSaveArchiveSummary,
|
ProfileSaveArchiveSummary,
|
||||||
|
ProfileWalletLedgerResponse,
|
||||||
RedeemProfileReferralInviteCodeResponse,
|
RedeemProfileReferralInviteCodeResponse,
|
||||||
RedeemProfileRewardCodeResponse,
|
RedeemProfileRewardCodeResponse,
|
||||||
} from '../../../packages/shared/src/contracts/runtime';
|
} from '../../../packages/shared/src/contracts/runtime';
|
||||||
@@ -101,6 +103,12 @@ export interface RpgEntryHomeViewProps {
|
|||||||
onSearchPublicCode?: (keyword: string) => void | Promise<void>;
|
onSearchPublicCode?: (keyword: string) => void | Promise<void>;
|
||||||
isSearchingPublicCode?: boolean;
|
isSearchingPublicCode?: boolean;
|
||||||
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
|
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
|
||||||
|
profilePlayStats?: ProfilePlayStatsResponse | null;
|
||||||
|
isProfilePlayStatsOpen?: boolean;
|
||||||
|
isProfilePlayStatsLoading?: boolean;
|
||||||
|
profilePlayStatsError?: string | null;
|
||||||
|
onCloseProfilePlayStats?: () => void;
|
||||||
|
onOpenPlayedWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||||
onRechargeSuccess?: () => void | Promise<void>;
|
onRechargeSuccess?: () => void | Promise<void>;
|
||||||
createTabContent?: ReactNode;
|
createTabContent?: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -814,6 +822,21 @@ function formatDashboardUpdatedAt(value: string | null | undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPlayedWorkType(value: string | null | undefined) {
|
||||||
|
const normalizedValue = (value ?? '').toLowerCase();
|
||||||
|
if (normalizedValue === 'puzzle') {
|
||||||
|
return '拼图';
|
||||||
|
}
|
||||||
|
if (normalizedValue === 'big_fish' || normalizedValue === 'big-fish') {
|
||||||
|
return '大鱼';
|
||||||
|
}
|
||||||
|
return 'RPG';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPlayedWorkId(work: ProfilePlayedWorkSummary) {
|
||||||
|
return work.profileId?.trim() || work.worldKey;
|
||||||
|
}
|
||||||
|
|
||||||
function buildPublicUserCode(user: AuthUser | null | undefined) {
|
function buildPublicUserCode(user: AuthUser | null | undefined) {
|
||||||
if (user?.publicUserCode?.trim()) {
|
if (user?.publicUserCode?.trim()) {
|
||||||
return user.publicUserCode.trim();
|
return user.publicUserCode.trim();
|
||||||
@@ -1248,6 +1271,108 @@ function ProfileReferralModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProfilePlayedWorksModal({
|
||||||
|
stats,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
onClose,
|
||||||
|
onOpenWork,
|
||||||
|
}: {
|
||||||
|
stats: ProfilePlayStatsResponse | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onOpenWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||||
|
}) {
|
||||||
|
const playedWorks = stats?.playedWorks ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/48 px-3 py-5">
|
||||||
|
<div className="relative max-h-[min(92vh,42rem)] w-full max-w-[34rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-3 top-3 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white/80 text-[#ff4056] shadow-sm"
|
||||||
|
aria-label="关闭玩过作品"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<div className="max-h-[min(92vh,42rem)] overflow-y-auto px-4 pb-5 pt-4 sm:px-5">
|
||||||
|
<div className="pr-10">
|
||||||
|
<div className="text-[10px] font-black tracking-[0.22em] text-[#ff4056]">
|
||||||
|
PLAYED
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-2xl font-black">玩过作品</div>
|
||||||
|
<div className="mt-2 inline-flex items-center gap-2 rounded-full border border-rose-100 bg-rose-50 px-3 py-1.5 text-xs font-bold text-zinc-600">
|
||||||
|
<Clock3 className="h-3.5 w-3.5 text-[#ff4056]" />
|
||||||
|
<span>{formatCompactPlayTime(stats?.totalPlayTimeMs ?? 0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="h-20 animate-pulse rounded-xl bg-zinc-100"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : playedWorks.length > 0 ? (
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
|
{playedWorks.map((work) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={`${work.worldKey}:${work.lastPlayedAt}`}
|
||||||
|
onClick={() => onOpenWork?.(work)}
|
||||||
|
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left transition hover:border-[#ff4056] hover:bg-white"
|
||||||
|
>
|
||||||
|
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="line-clamp-1 text-base font-black text-zinc-950">
|
||||||
|
{work.worldTitle}
|
||||||
|
</div>
|
||||||
|
{work.worldSubtitle ? (
|
||||||
|
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||||
|
{work.worldSubtitle}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-bold text-[#ff4056]">
|
||||||
|
{formatPlayedWorkType(work.worldType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
|
||||||
|
<span className="truncate">
|
||||||
|
作品号 {formatPlayedWorkId(work)}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">
|
||||||
|
最近 {formatSnapshotTime(work.lastPlayedAt)}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">
|
||||||
|
时长 {formatCompactPlayTime(work.lastObservedPlayTimeMs)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-5 rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-sm text-zinc-600">
|
||||||
|
暂无玩过作品
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function RpgEntryHomeView({
|
export function RpgEntryHomeView({
|
||||||
activeTab,
|
activeTab,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
@@ -1272,6 +1397,12 @@ export function RpgEntryHomeView({
|
|||||||
onSearchPublicCode,
|
onSearchPublicCode,
|
||||||
isSearchingPublicCode = false,
|
isSearchingPublicCode = false,
|
||||||
onOpenProfileDashboardCard,
|
onOpenProfileDashboardCard,
|
||||||
|
profilePlayStats = null,
|
||||||
|
isProfilePlayStatsOpen = false,
|
||||||
|
isProfilePlayStatsLoading = false,
|
||||||
|
profilePlayStatsError = null,
|
||||||
|
onCloseProfilePlayStats,
|
||||||
|
onOpenPlayedWork,
|
||||||
onRechargeSuccess,
|
onRechargeSuccess,
|
||||||
createTabContent,
|
createTabContent,
|
||||||
}: RpgEntryHomeViewProps) {
|
}: RpgEntryHomeViewProps) {
|
||||||
@@ -2301,6 +2432,15 @@ export function RpgEntryHomeView({
|
|||||||
onSubmitRedeem={submitReferralInviteCode}
|
onSubmitRedeem={submitReferralInviteCode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isProfilePlayStatsOpen ? (
|
||||||
|
<ProfilePlayedWorksModal
|
||||||
|
stats={profilePlayStats}
|
||||||
|
isLoading={isProfilePlayStatsLoading}
|
||||||
|
error={profilePlayStatsError}
|
||||||
|
onClose={onCloseProfilePlayStats ?? (() => undefined)}
|
||||||
|
onOpenWork={onOpenPlayedWork}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{isWalletLedgerOpen ? (
|
{isWalletLedgerOpen ? (
|
||||||
<WalletLedgerModal
|
<WalletLedgerModal
|
||||||
ledger={walletLedger}
|
ledger={walletLedger}
|
||||||
@@ -2414,6 +2554,15 @@ export function RpgEntryHomeView({
|
|||||||
onSubmitRedeem={submitReferralInviteCode}
|
onSubmitRedeem={submitReferralInviteCode}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isProfilePlayStatsOpen ? (
|
||||||
|
<ProfilePlayedWorksModal
|
||||||
|
stats={profilePlayStats}
|
||||||
|
isLoading={isProfilePlayStatsLoading}
|
||||||
|
error={profilePlayStatsError}
|
||||||
|
onClose={onCloseProfilePlayStats ?? (() => undefined)}
|
||||||
|
onOpenWork={onOpenPlayedWork}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{isWalletLedgerOpen ? (
|
{isWalletLedgerOpen ? (
|
||||||
<WalletLedgerModal
|
<WalletLedgerModal
|
||||||
ledger={walletLedger}
|
ledger={walletLedger}
|
||||||
|
|||||||
33
src/services/big-fish-runtime/bigFishRuntimeClient.ts
Normal file
33
src/services/big-fish-runtime/bigFishRuntimeClient.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import type {
|
||||||
|
BigFishSessionResponse,
|
||||||
|
RecordBigFishPlayRequest,
|
||||||
|
} from '../../../packages/shared/src/contracts/bigFish';
|
||||||
|
import { type ApiRetryOptions, requestJson } from '../apiClient';
|
||||||
|
|
||||||
|
const BIG_FISH_RUNTIME_WRITE_RETRY: ApiRetryOptions = {
|
||||||
|
maxRetries: 1,
|
||||||
|
baseDelayMs: 120,
|
||||||
|
maxDelayMs: 360,
|
||||||
|
retryUnsafeMethods: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上报大鱼吃小鱼正式游玩。elapsedMs 为 0 时仅标记玩过作品。
|
||||||
|
*/
|
||||||
|
export function recordBigFishPlay(
|
||||||
|
sessionId: string,
|
||||||
|
payload: RecordBigFishPlayRequest,
|
||||||
|
) {
|
||||||
|
return requestJson<BigFishSessionResponse>(
|
||||||
|
`/api/runtime/big-fish/sessions/${encodeURIComponent(sessionId)}/play`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
},
|
||||||
|
'记录大鱼吃小鱼游玩失败',
|
||||||
|
{
|
||||||
|
retry: BIG_FISH_RUNTIME_WRITE_RETRY,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export {
|
|||||||
advanceLocalBigFishRuntimeRun,
|
advanceLocalBigFishRuntimeRun,
|
||||||
startLocalBigFishRuntimeRun,
|
startLocalBigFishRuntimeRun,
|
||||||
} from './bigFishLocalRuntime';
|
} from './bigFishLocalRuntime';
|
||||||
|
export { recordBigFishPlay } from './bigFishRuntimeClient';
|
||||||
|
|||||||
Reference in New Issue
Block a user