This commit is contained in:
2026-04-26 14:27:48 +08:00
parent f68f4914ec
commit ea33413187
155 changed files with 8130 additions and 1740 deletions

View File

@@ -0,0 +1,28 @@
{
"next_user_id": 2,
"users_by_username": {
"phone_00000002": {
"user": {
"id": "user_00000001",
"public_user_code": "SY-00000001",
"username": "phone_00000002",
"display_name": "138****8111",
"phone_number_masked": "138****8111",
"login_method": "Phone",
"binding_status": "Active",
"wechat_bound": false,
"token_version": 1
},
"password_hash": "$argon2id$v=19$m=19456,t=2,p=1$qnArSgOrZvcQxap4KAMMnA$+K+gQgf7h0jQibJLuvAlOeHnNNYutTvLVDAyo1hqS/o",
"password_login_enabled": false,
"phone_number": "+8613800138111"
}
},
"phone_to_user_id": {
"+8613800138111": "user_00000001"
},
"sessions_by_id": {},
"session_id_by_refresh_token_hash": {},
"wechat_identity_by_provider_uid": {},
"user_id_by_provider_union_id": {}
}

View File

@@ -0,0 +1,28 @@
{
"next_user_id": 2,
"users_by_username": {
"phone_00000002": {
"user": {
"id": "user_00000001",
"public_user_code": "SY-00000001",
"username": "phone_00000002",
"display_name": "138****8112",
"phone_number_masked": "138****8112",
"login_method": "Phone",
"binding_status": "Active",
"wechat_bound": false,
"token_version": 1
},
"password_hash": "$argon2id$v=19$m=19456,t=2,p=1$0HR2g/fKOw9EFHz7BuYtGg$cpXb5KBwbEXPxPJHA4Bk1U7NtM97GhGTq7VK6jCJ+lA",
"password_login_enabled": false,
"phone_number": "+8613800138112"
}
},
"phone_to_user_id": {
"+8613800138112": "user_00000001"
},
"sessions_by_id": {},
"session_id_by_refresh_token_hash": {},
"wechat_identity_by_provider_uid": {},
"user_id_by_provider_union_id": {}
}

View File

@@ -0,0 +1,28 @@
{
"next_user_id": 2,
"users_by_username": {
"phone_00000002": {
"user": {
"id": "user_00000001",
"public_user_code": "SY-00000001",
"username": "phone_00000002",
"display_name": "138****8110",
"phone_number_masked": "138****8110",
"login_method": "Phone",
"binding_status": "Active",
"wechat_bound": false,
"token_version": 1
},
"password_hash": "$argon2id$v=19$m=19456,t=2,p=1$fEeSrVyialDeb8rarDSpdA$HFihZiuCOyaz8F5iNukmobeiHI/EpYWdeQzhbIYR4zk",
"password_login_enabled": false,
"phone_number": "+8613800138110"
}
},
"phone_to_user_id": {
"+8613800138110": "user_00000001"
},
"sessions_by_id": {},
"session_id_by_refresh_token_hash": {},
"wechat_identity_by_provider_uid": {},
"user_id_by_provider_union_id": {}
}

View File

@@ -0,0 +1,56 @@
{
"next_user_id": 2,
"users_by_username": {
"phone_00000002": {
"user": {
"id": "user_00000001",
"public_user_code": "SY-00000001",
"username": "phone_00000002",
"display_name": "138****8000",
"phone_number_masked": "138****8000",
"login_method": "Phone",
"binding_status": "Active",
"wechat_bound": false,
"token_version": 1
},
"password_hash": "$argon2id$v=19$m=19456,t=2,p=1$hoXmK/LzABj2QfWZSO3SNA$Qg71V2iZCPyLOsoQLffiCv3KPkWVNSAsP6IooTIXi/w",
"password_login_enabled": false,
"phone_number": "+8613800138000"
}
},
"phone_to_user_id": {
"+8613800138000": "user_00000001"
},
"sessions_by_id": {
"usess_52522126b58d40e3b9e503808dd11e2c": {
"session": {
"session_id": "usess_52522126b58d40e3b9e503808dd11e2c",
"user_id": "user_00000001",
"refresh_token_hash": "f42140526caea3e4a9f533bcc2d8799feae4f96769ea975ef771b1ae11e4dbe9",
"issued_by_provider": "Phone",
"client_info": {
"client_type": "web_browser",
"client_runtime": "unknown",
"client_platform": "unknown",
"client_instance_id": null,
"device_fingerprint": null,
"device_display_name": "未知设备 / 未知客户端",
"mini_program_app_id": null,
"mini_program_env": null,
"user_agent": null,
"ip": null
},
"expires_at": "2026-05-25T15:41:01.0856147Z",
"revoked_at": null,
"created_at": "2026-04-25T15:41:01.0856147Z",
"updated_at": "2026-04-25T15:41:01.0856147Z",
"last_seen_at": "2026-04-25T15:41:01.0856147Z"
}
}
},
"session_id_by_refresh_token_hash": {
"f42140526caea3e4a9f533bcc2d8799feae4f96769ea975ef771b1ae11e4dbe9": "usess_52522126b58d40e3b9e503808dd11e2c"
},
"wechat_identity_by_provider_uid": {},
"user_id_by_provider_union_id": {}
}

View File

@@ -95,7 +95,8 @@ use crate::{
runtime_inventory::get_runtime_inventory_state,
runtime_profile::{
create_profile_recharge_order, get_profile_dashboard, get_profile_play_stats,
get_profile_recharge_center, get_profile_wallet_ledger,
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_wallet_ledger,
redeem_profile_referral_invite_code,
},
runtime_save::{
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
@@ -820,6 +821,34 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/referrals/invite-center",
get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/referrals/invite-center",
get(get_profile_referral_invite_center).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/referrals/redeem-code",
post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/referrals/redeem-code",
post(redeem_profile_referral_invite_code).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/profile/play-stats",
get(get_profile_play_stats).route_layer(middleware::from_fn_with_state(

View File

@@ -434,13 +434,7 @@ pub async fn execute_big_fish_action(
let now = current_utc_micros();
let session = match payload.action.trim() {
"big_fish_compile_draft" => {
compile_big_fish_draft_with_all_assets(
&state,
session_id,
owner_user_id,
now,
)
.await
compile_big_fish_draft_with_all_assets(&state, session_id, owner_user_id, now).await
}
"big_fish_generate_level_main_image" => {
let asset_url = generate_big_fish_formal_asset(

View File

@@ -74,14 +74,9 @@ pub async fn generate_character_visual(
// 旧资产工坊接口没有显式 Bearer 头Rust 兼容层先使用工具用户归属,避免破坏现有前端调用。
let owner_user_id = "asset-tool".to_string();
let task_id = generate_ai_task_id(current_utc_micros());
let prompt = build_character_visual_prompt(
payload.prompt_text.as_str(),
payload.character_brief_text.as_deref(),
);
let fallback_prompt = build_fallback_moderation_safe_character_visual_prompt(
payload.prompt_text.as_str(),
payload.character_brief_text.as_deref(),
);
let prompt = build_character_visual_prompt(payload.prompt_text.as_str());
let fallback_prompt =
build_fallback_moderation_safe_character_visual_prompt(payload.prompt_text.as_str());
let character_id = normalize_required_text(payload.character_id.as_str(), "character");
let model = normalize_required_text(payload.image_model.as_str(), CHARACTER_VISUAL_MODEL);
let size = normalize_required_text(payload.size.as_str(), "1024*1024");
@@ -296,27 +291,20 @@ pub(crate) async fn generate_character_primary_visual_for_profile(
owner_user_id: &str,
character_id: &str,
prompt_text: &str,
character_brief_text: Option<&str>,
) -> Result<GeneratedCharacterPrimaryVisual, AppError> {
let payload = CharacterVisualGenerateRequest {
character_id: character_id.to_string(),
source_mode: shared_contracts::assets::CharacterVisualSourceMode::TextToImage,
prompt_text: prompt_text.to_string(),
character_brief_text: character_brief_text.map(ToOwned::to_owned),
reference_image_data_urls: Vec::new(),
candidate_count: 1,
image_model: CHARACTER_VISUAL_MODEL.to_string(),
size: "1024*1024".to_string(),
};
let task_id = generate_ai_task_id(current_utc_micros());
let prompt = build_character_visual_prompt(
payload.prompt_text.as_str(),
payload.character_brief_text.as_deref(),
);
let fallback_prompt = build_fallback_moderation_safe_character_visual_prompt(
payload.prompt_text.as_str(),
payload.character_brief_text.as_deref(),
);
let prompt = build_character_visual_prompt(payload.prompt_text.as_str());
let fallback_prompt =
build_fallback_moderation_safe_character_visual_prompt(payload.prompt_text.as_str());
let character_id = normalize_required_text(payload.character_id.as_str(), "character");
let model = normalize_required_text(payload.image_model.as_str(), CHARACTER_VISUAL_MODEL);
let size = normalize_required_text(payload.size.as_str(), "1024*1024");
@@ -2068,7 +2056,7 @@ mod tests {
#[test]
fn build_character_visual_prompt_keeps_generation_constraints() {
let prompt = build_character_visual_prompt("潮雾港向导", Some("旧港守望者"));
let prompt = build_character_visual_prompt("潮雾港向导");
assert!(prompt.contains("潮雾港向导"));
assert!(prompt.contains("右向斜侧身"));
@@ -2077,10 +2065,8 @@ mod tests {
#[test]
fn fallback_character_visual_prompt_removes_risky_specific_names() {
let prompt = build_fallback_moderation_safe_character_visual_prompt(
"艾瑞克,银发剑士,红色长披风",
Some("某知名设定参考"),
);
let prompt =
build_fallback_moderation_safe_character_visual_prompt("艾瑞克,银发剑士,红色长披风");
assert!(prompt.contains("原创"));
assert!(prompt.contains("不参考任何现有"));

View File

@@ -1613,7 +1613,6 @@ async fn generate_draft_foundation_role_visuals(
task_owner_user_id.as_str(),
role_ref.role_id.as_str(),
role_ref.prompt.as_str(),
Some(role_ref.name.as_str()),
)
.await
};
@@ -2429,8 +2428,8 @@ fn log_custom_world_publish_gate_diagnostics(
has_draft_profile = session.draft_profile.as_object().map(|value| !value.is_empty()).unwrap_or(false),
has_result_preview = session.result_preview.is_some(),
preview_source = session.result_preview.as_ref().and_then(|value| value.get("source")).and_then(serde_json::Value::as_str).unwrap_or(""),
has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise.hook", "settingText"]),
has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]),
has_world_hook = has_custom_world_publish_text(profile, &["worldHook", "creatorIntent.worldHook", "anchorContent.worldPromise", "anchorContent.worldPromise.hook", "settingText"]),
has_player_premise = has_custom_world_publish_text(profile, &["playerPremise", "creatorIntent.playerPremise", "anchorContent.playerEntryPoint", "anchorContent.playerEntryPoint.openingIdentity", "anchorContent.playerEntryPoint.openingProblem", "anchorContent.playerEntryPoint.entryMotivation"]),
has_core_conflicts = has_custom_world_non_empty_text_array(profile, "coreConflicts"),
has_main_chapter = has_custom_world_array(profile, "chapters") || has_custom_world_array(profile, "sceneChapterBlueprints") || has_custom_world_array(profile, "sceneChapters"),
has_scene_act = has_custom_world_scene_act(profile),

View File

@@ -99,113 +99,25 @@ pub(crate) struct PromptDynamicStateInference {
judgement_summary: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct WorldPromiseValue {
#[serde(default)]
hook: String,
#[serde(default)]
differentiator: String,
#[serde(default)]
desired_experience: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerFantasyValue {
#[serde(default)]
player_role: String,
#[serde(default)]
core_pursuit: String,
#[serde(default)]
fear_of_loss: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ThemeBoundaryValue {
#[serde(default)]
tone_keywords: Vec<String>,
#[serde(default)]
aesthetic_directives: Vec<String>,
#[serde(default)]
forbidden_directives: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerEntryPointValue {
#[serde(default)]
opening_identity: String,
#[serde(default)]
opening_problem: String,
#[serde(default)]
entry_motivation: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CoreConflictValue {
#[serde(default)]
surface_conflicts: Vec<String>,
#[serde(default)]
hidden_crisis: String,
#[serde(default)]
first_touched_conflict: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyRelationshipValue {
#[serde(default)]
pairs: String,
#[serde(default)]
relationship_type: String,
#[serde(default)]
secret_or_cost: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HiddenLineValue {
#[serde(default)]
hidden_truths: Vec<String>,
#[serde(default)]
misdirection_hints: Vec<String>,
#[serde(default)]
reveal_pacing: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IconicElementValue {
#[serde(default)]
iconic_motifs: Vec<String>,
#[serde(default)]
institutions_or_artifacts: Vec<String>,
#[serde(default)]
hard_rules: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EightAnchorContent {
#[serde(default)]
world_promise: Option<WorldPromiseValue>,
world_promise: Option<String>,
#[serde(default)]
player_fantasy: Option<PlayerFantasyValue>,
player_fantasy: Option<String>,
#[serde(default)]
theme_boundary: Option<ThemeBoundaryValue>,
theme_boundary: Option<String>,
#[serde(default)]
player_entry_point: Option<PlayerEntryPointValue>,
player_entry_point: Option<String>,
#[serde(default)]
core_conflict: Option<CoreConflictValue>,
core_conflict: Option<String>,
#[serde(default)]
key_relationships: Vec<KeyRelationshipValue>,
key_relationships: Option<String>,
#[serde(default)]
hidden_lines: Option<HiddenLineValue>,
hidden_lines: Option<String>,
#[serde(default)]
iconic_elements: Option<IconicElementValue>,
iconic_elements: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
@@ -814,137 +726,127 @@ fn build_chat_history(messages: &[CustomWorldAgentMessageRecord]) -> Vec<JsonVal
}
fn normalize_eight_anchor_content(value: &JsonValue) -> EightAnchorContent {
serde_json::from_value::<EightAnchorContent>(value.clone()).unwrap_or_default()
// Agent session 的新结构要求每个锚点只保存一段文本;这里兼容旧对象/数组存档,
// 读取时压缩成单字段,写回时仍由 EightAnchorContent 序列化为新结构。
EightAnchorContent {
world_promise: normalize_anchor_text(value.get("worldPromise")),
player_fantasy: normalize_anchor_text(value.get("playerFantasy")),
theme_boundary: normalize_anchor_text(value.get("themeBoundary")),
player_entry_point: normalize_anchor_text(value.get("playerEntryPoint")),
core_conflict: normalize_anchor_text(value.get("coreConflict")),
key_relationships: normalize_anchor_text(value.get("keyRelationships")),
hidden_lines: normalize_anchor_text(value.get("hiddenLines")),
iconic_elements: normalize_anchor_text(value.get("iconicElements")),
}
}
fn normalize_anchor_text(value: Option<&JsonValue>) -> Option<String> {
let normalized = compact_json_anchor_text(value?)?;
Some(clamp_text(normalized.as_str(), 180))
}
fn compact_json_anchor_text(value: &JsonValue) -> Option<String> {
match value {
JsonValue::Null => None,
JsonValue::String(text) => {
let normalized = text.split_whitespace().collect::<Vec<_>>().join(" ");
(!normalized.trim().is_empty()).then_some(normalized.trim().to_string())
}
JsonValue::Array(items) => {
let values = items
.iter()
.filter_map(compact_json_anchor_text)
.collect::<Vec<_>>();
let compacted = dedupe_string_list(values, 8).join("");
(!compacted.trim().is_empty()).then_some(compacted)
}
JsonValue::Object(object) => {
let values = object
.values()
.filter_map(compact_json_anchor_text)
.collect::<Vec<_>>();
let compacted = dedupe_string_list(values, 8).join("");
(!compacted.trim().is_empty()).then_some(compacted)
}
JsonValue::Bool(value) => Some(value.to_string()),
JsonValue::Number(value) => Some(value.to_string()),
}
}
fn split_anchor_phrases(value: Option<&str>) -> Vec<String> {
value
.unwrap_or_default()
.split(['', ';', '、', ',', '', '\n'])
.map(str::trim)
.filter(|item| !item.is_empty())
.map(str::to_string)
.collect()
}
fn build_creator_intent_from_eight_anchor_content(
anchor_content: &EightAnchorContent,
) -> CreatorIntentRecord {
let key_characters = anchor_content
let key_relationship_text = anchor_content
.key_relationships
.iter()
.enumerate()
.map(|(index, entry)| {
let (lead_name, relation_to_player) = split_relationship_pair(entry.pairs.as_str());
CreatorCharacterSeedRecord {
id: format!("creator-character-{}", index + 1),
name: if lead_name.is_empty() {
format!("关键人物{}", index + 1)
} else {
lead_name
},
role: entry.relationship_type.clone(),
public_mask: String::new(),
hidden_hook: entry.secret_or_cost.clone(),
relation_to_player,
notes: String::new(),
}
})
.collect::<Vec<_>>();
let core_conflicts = anchor_content
.core_conflict
.as_ref()
.map(|value| {
value
.surface_conflicts
.iter()
.cloned()
.chain(
(!value.hidden_crisis.trim().is_empty()).then_some(value.hidden_crisis.clone()),
)
.collect::<Vec<_>>()
})
.unwrap_or_default();
.as_deref()
.unwrap_or_default()
.trim()
.to_string();
let key_characters = if key_relationship_text.is_empty() {
Vec::new()
} else {
let (lead_name, relation_to_player) =
split_relationship_pair(key_relationship_text.as_str());
vec![CreatorCharacterSeedRecord {
id: "creator-character-1".to_string(),
name: if lead_name.is_empty() {
"关键人物1".to_string()
} else {
lead_name
},
role: key_relationship_text.clone(),
public_mask: String::new(),
hidden_hook: key_relationship_text.clone(),
relation_to_player,
notes: String::new(),
}]
};
CreatorIntentRecord {
source_mode: "freeform".to_string(),
raw_setting_text: compact_lines([
anchor_content
.world_promise
.as_ref()
.map(|value| value.differentiator.as_str()),
anchor_content
.player_fantasy
.as_ref()
.map(|value| value.core_pursuit.as_str()),
anchor_content
.hidden_lines
.as_ref()
.and_then(|value| value.hidden_truths.first().map(String::as_str)),
anchor_content.world_promise.as_deref(),
anchor_content.player_fantasy.as_deref(),
anchor_content.hidden_lines.as_deref(),
]),
world_hook: compact_lines([
anchor_content
.world_promise
.as_ref()
.map(|value| value.hook.as_str()),
anchor_content
.world_promise
.as_ref()
.map(|value| value.differentiator.as_str()),
]),
theme_keywords: anchor_content
.theme_boundary
.as_ref()
.map(|value| value.tone_keywords.clone())
.unwrap_or_default(),
tone_directives: anchor_content
.theme_boundary
.as_ref()
.map(|value| value.aesthetic_directives.clone())
.unwrap_or_default(),
world_hook: anchor_content.world_promise.clone().unwrap_or_default(),
theme_keywords: split_anchor_phrases(anchor_content.theme_boundary.as_deref()),
tone_directives: split_anchor_phrases(anchor_content.theme_boundary.as_deref()),
player_premise: compact_lines([
anchor_content
.player_fantasy
.as_ref()
.map(|value| value.player_role.as_str()),
anchor_content
.player_entry_point
.as_ref()
.map(|value| value.opening_identity.as_str()),
anchor_content.player_fantasy.as_deref(),
anchor_content.player_entry_point.as_deref(),
]),
opening_situation: compact_lines([
anchor_content
.player_entry_point
.as_ref()
.map(|value| value.opening_problem.as_str()),
anchor_content
.player_entry_point
.as_ref()
.map(|value| value.entry_motivation.as_str()),
]),
core_conflicts: dedupe_string_list(core_conflicts, 6),
opening_situation: anchor_content
.player_entry_point
.clone()
.unwrap_or_default(),
core_conflicts: dedupe_string_list(
split_anchor_phrases(anchor_content.core_conflict.as_deref()),
6,
),
key_characters,
key_landmarks: Vec::new(),
iconic_elements: dedupe_string_list(
anchor_content
.iconic_elements
.as_ref()
.map(|value| {
value
.iconic_motifs
.iter()
.cloned()
.chain(value.institutions_or_artifacts.iter().cloned())
.collect::<Vec<_>>()
})
.unwrap_or_default(),
split_anchor_phrases(anchor_content.iconic_elements.as_deref()),
8,
),
forbidden_directives: dedupe_string_list(
anchor_content
.theme_boundary
.as_ref()
.map(|value| value.forbidden_directives.clone())
.unwrap_or_default()
split_anchor_phrases(anchor_content.theme_boundary.as_deref())
.into_iter()
.chain(
anchor_content
.iconic_elements
.as_ref()
.map(|value| value.hard_rules.clone())
.unwrap_or_default(),
)
.chain(split_anchor_phrases(
anchor_content.iconic_elements.as_deref(),
))
.filter(|value| contains_any(value, &["避免", "不要", "禁止", "不能", "硬规则"]))
.collect::<Vec<_>>(),
8,
),
@@ -1370,36 +1272,12 @@ fn detect_drift_risk(
let filled_count = [
anchor_content.world_promise.is_some(),
anchor_content.player_fantasy.is_some(),
anchor_content
.theme_boundary
.as_ref()
.map(|value| {
!value.tone_keywords.is_empty()
|| !value.aesthetic_directives.is_empty()
|| !value.forbidden_directives.is_empty()
})
.unwrap_or(false),
anchor_content.theme_boundary.is_some(),
anchor_content.player_entry_point.is_some(),
anchor_content.core_conflict.is_some(),
!anchor_content.key_relationships.is_empty(),
anchor_content
.hidden_lines
.as_ref()
.map(|value| {
!value.hidden_truths.is_empty()
|| !value.misdirection_hints.is_empty()
|| !value.reveal_pacing.trim().is_empty()
})
.unwrap_or(false),
anchor_content
.iconic_elements
.as_ref()
.map(|value| {
!value.iconic_motifs.is_empty()
|| !value.institutions_or_artifacts.is_empty()
|| !value.hard_rules.is_empty()
})
.unwrap_or(false),
anchor_content.key_relationships.is_some(),
anchor_content.hidden_lines.is_some(),
anchor_content.iconic_elements.is_some(),
]
.iter()
.filter(|value| **value)

View File

@@ -2548,26 +2548,24 @@ mod tests {
name: Some("礁石神殿".to_string()),
description: Some("古老礁石上的半沉神殿。".to_string()),
};
let manual_prompt = build_custom_world_scene_image_prompt(
SceneImagePromptParams {
profile: SceneImagePromptProfile {
name: profile_input.name.as_deref().unwrap_or_default(),
subtitle: profile_input.subtitle.as_deref().unwrap_or_default(),
tone: profile_input.tone.as_deref().unwrap_or_default(),
player_goal: profile_input.player_goal.as_deref().unwrap_or_default(),
summary: profile_input.summary.as_deref().unwrap_or_default(),
setting_text: profile_input.setting_text.as_deref().unwrap_or_default(),
},
landmark: SceneImagePromptLandmark {
name: landmark.name.as_deref().unwrap_or_default(),
description: landmark.description.as_deref().unwrap_or_default(),
},
user_prompt,
has_reference_image: false,
fallback_landmark_name: Some("礁石神殿"),
fallback_world_name: "雾海群岛",
let manual_prompt = build_custom_world_scene_image_prompt(SceneImagePromptParams {
profile: SceneImagePromptProfile {
name: profile_input.name.as_deref().unwrap_or_default(),
subtitle: profile_input.subtitle.as_deref().unwrap_or_default(),
tone: profile_input.tone.as_deref().unwrap_or_default(),
player_goal: profile_input.player_goal.as_deref().unwrap_or_default(),
summary: profile_input.summary.as_deref().unwrap_or_default(),
setting_text: profile_input.setting_text.as_deref().unwrap_or_default(),
},
);
landmark: SceneImagePromptLandmark {
name: landmark.name.as_deref().unwrap_or_default(),
description: landmark.description.as_deref().unwrap_or_default(),
},
user_prompt,
has_reference_image: false,
fallback_landmark_name: Some("礁石神殿"),
fallback_world_name: "雾海群岛",
});
let normalized = normalize_scene_image_request(CustomWorldSceneImageRequest {
profile_id: Some("profile_001".to_string()),

View File

@@ -933,7 +933,7 @@ fn build_scene_act_blueprint_from_landmark(
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("");
.map(ToOwned::to_owned);
let opposite_npc_id = scene_npc_names.first().cloned().unwrap_or_default();
let event_description = act_events
.get(act_index)
@@ -944,13 +944,20 @@ fn build_scene_act_blueprint_from_landmark(
.unwrap_or_else(|| {
build_default_act_event_description(scene_summary, opposite_npc_id.as_str(), act_index)
});
// 缺失时保留空值,让后续生图前校验暴露底稿质量问题。
let background_prompt = prompt.unwrap_or_else(|| {
build_default_act_background_prompt(
scene_summary,
opposite_npc_id.as_str(),
event_description.as_str(),
act_index,
)
});
json!({
"id": format!("{}-act-{}", scene_id, act_index + 1),
"sceneId": scene_id,
"title": act_title,
"summary": scene_summary,
"backgroundPromptText": prompt,
"backgroundPromptText": background_prompt,
"encounterNpcIds": scene_npc_names,
"primaryNpcId": opposite_npc_id,
"oppositeNpcId": opposite_npc_id,
@@ -982,11 +989,46 @@ fn build_default_act_event_description(
} else {
scene_summary.trim()
};
match act_index {
0 => format!(
"第1幕中{}先露出与{}有关的异常线索,玩家必须确认局势入口。",
role_text, scene_text
),
1 => format!(
"第2幕中{}的立场或阻碍让{}升级,玩家必须在压力下作出判断。",
role_text, scene_text
),
_ => format!(
"第3幕中{}{}推向高潮,玩家必须面对关键抉择或直接后果。",
role_text, scene_text
),
}
}
fn build_default_act_background_prompt(
scene_summary: &str,
opposite_npc_id: &str,
event_description: &str,
act_index: usize,
) -> String {
let role_text = if opposite_npc_id.trim().is_empty() {
"当前场景关键角色"
} else {
opposite_npc_id.trim()
};
let scene_text = if scene_summary.trim().is_empty() {
"场景内的主线压力"
} else {
scene_summary.trim()
};
let phase_text = match act_index {
0 => "铺垫阶段",
1 => "冲突升级阶段",
_ => "高潮阶段",
};
// 中文注释:幕背景默认值直接吃同幕事件和角色,避免前端再拼规则说明句。
format!(
"{}幕中,玩家与{}正面接触,围绕{}处理一件会改变局势走向的事件",
act_index + 1,
role_text,
scene_text,
"{scene_text}{phase_text}画面,{role_text}与玩家隔着可站立空间形成对峙,环境里保留“{event_description}”的冲突痕迹与清晰氛围"
)
}
@@ -1080,11 +1122,16 @@ fn normalize_framework_shape(framework: &mut JsonValue, setting_text: &str) {
JsonValue::Array(
(0..3)
.map(|index| {
JsonValue::String(format!(
"{}{}幕,{},画面保留玩家站位、近景可交互物件与远景世界压力。",
camp_name,
index + 1,
camp_description,
let event_description = build_default_act_event_description(
camp_description.as_str(),
"开局关键角色",
index,
);
JsonValue::String(build_default_act_background_prompt(
camp_description.as_str(),
"开局关键角色",
event_description.as_str(),
index,
))
})
.collect(),
@@ -1382,17 +1429,12 @@ fn normalize_scene_act_blueprint(act: JsonValue, index: usize) -> JsonValue {
.unwrap_or_else(|| "当前幕推动场景内的主线压力。".to_string());
object.insert("title".to_string(), JsonValue::String(title.clone()));
object.insert("summary".to_string(), JsonValue::String(summary.clone()));
let background_prompt = object
let raw_background_prompt = object
.get("backgroundPromptText")
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_default();
object.insert(
"backgroundPromptText".to_string(),
JsonValue::String(background_prompt),
);
.map(ToOwned::to_owned);
let encounter_npc_ids = object
.get("encounterNpcIds")
.and_then(JsonValue::as_array)
@@ -1434,6 +1476,18 @@ fn normalize_scene_act_blueprint(act: JsonValue, index: usize) -> JsonValue {
.unwrap_or_else(|| {
build_default_act_event_description(summary.as_str(), opposite_npc_id.as_str(), index)
});
let background_prompt = raw_background_prompt.unwrap_or_else(|| {
build_default_act_background_prompt(
summary.as_str(),
opposite_npc_id.as_str(),
event_description.as_str(),
index,
)
});
object.insert(
"backgroundPromptText".to_string(),
JsonValue::String(background_prompt),
);
object.insert(
"encounterNpcIds".to_string(),
JsonValue::Array(encounter_npc_ids),
@@ -1468,15 +1522,25 @@ fn build_fallback_scene_act() -> JsonValue {
}
fn build_fallback_scene_act_with_index(index: usize) -> JsonValue {
let event_description = build_default_act_event_description(
"玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。",
"",
index,
);
json!({
"id": format!("scene-act-{}", index + 1),
"title": if index == 0 { "开场场景幕".to_string() } else { format!("{}", index + 1) },
"summary": "玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。",
"backgroundPromptText": "",
"backgroundPromptText": build_default_act_background_prompt(
"玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。",
"",
event_description.as_str(),
index,
),
"encounterNpcIds": [],
"primaryNpcId": "",
"oppositeNpcId": "",
"eventDescription": build_default_act_event_description("玩家被推入第一波局势,必须先确认站位、威胁和下一步追查方向。", "", index),
"eventDescription": event_description,
})
}
@@ -1489,12 +1553,21 @@ fn derive_world_name(
session
.anchor_content
.get("worldPromise")
.and_then(JsonValue::as_object)
.and_then(|entry| entry.get("hook"))
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
session
.anchor_content
.get("worldPromise")
.and_then(JsonValue::as_object)
.and_then(|entry| entry.get("hook"))
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
})
.unwrap_or_else(|| "未命名世界草稿".to_string())
}
@@ -1508,12 +1581,21 @@ fn derive_world_hook(
session
.anchor_content
.get("worldPromise")
.and_then(JsonValue::as_object)
.and_then(|entry| entry.get("hook"))
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
session
.anchor_content
.get("worldPromise")
.and_then(JsonValue::as_object)
.and_then(|entry| entry.get("hook"))
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
})
.unwrap_or_else(|| {
"这个世界正被一条持续扩大的主线危机推向失衡,而玩家会被卷入其中心。".to_string()
@@ -1529,28 +1611,37 @@ fn derive_player_premise(
session
.anchor_content
.get("playerEntryPoint")
.and_then(JsonValue::as_object)
.map(|entry| {
let identity = entry
.get("openingIdentity")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
let problem = entry
.get("openingProblem")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
let motivation = entry
.get("entryMotivation")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
[identity, problem, motivation]
.into_iter()
.filter(|value| !value.is_empty())
.collect::<Vec<_>>()
.join("")
.and_then(JsonValue::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
session
.anchor_content
.get("playerEntryPoint")
.and_then(JsonValue::as_object)
.map(|entry| {
let identity = entry
.get("openingIdentity")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
let problem = entry
.get("openingProblem")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
let motivation = entry
.get("entryMotivation")
.and_then(JsonValue::as_str)
.map(str::trim)
.unwrap_or_default();
[identity, problem, motivation]
.into_iter()
.filter(|value| !value.is_empty())
.collect::<Vec<_>>()
.join("")
})
})
.filter(|value| !value.trim().is_empty())
})
@@ -1740,7 +1831,7 @@ mod tests {
}
#[test]
fn normalize_scene_act_keeps_missing_background_prompt_empty() {
fn normalize_scene_act_fills_missing_background_prompt_from_event() {
let act = normalize_scene_act_blueprint(
json!({
"title": "第1幕",
@@ -1749,7 +1840,15 @@ mod tests {
0,
);
assert_eq!(act.get("backgroundPromptText"), Some(&json!("")));
assert!(
act.get("backgroundPromptText")
.and_then(JsonValue::as_str)
.is_some_and(|value| {
value.contains("铺垫阶段")
&& value.contains("玩家进入雾港码头")
&& value.contains("冲突痕迹")
})
);
assert!(
act.get("eventDescription")
.and_then(JsonValue::as_str)

View File

@@ -182,50 +182,28 @@ pub(crate) const OUTPUT_CONTRACT_REMINDER: &str = r#"请严格按以下 JSON 结
"replyText": "",
"progressPercent": 0,
"nextAnchorContent": {
"worldPromise": {
"hook": "",
"differentiator": "",
"desiredExperience": ""
},
"playerFantasy": {
"playerRole": "",
"corePursuit": "",
"fearOfLoss": ""
},
"themeBoundary": {
"toneKeywords": [],
"aestheticDirectives": [],
"forbiddenDirectives": []
},
"playerEntryPoint": {
"openingIdentity": "",
"openingProblem": "",
"entryMotivation": ""
},
"coreConflict": {
"surfaceConflicts": [],
"hiddenCrisis": "",
"firstTouchedConflict": ""
},
"keyRelationships": [
{
"pairs": "",
"relationshipType": "",
"secretOrCost": ""
}
],
"hiddenLines": {
"hiddenTruths": [],
"misdirectionHints": [],
"revealPacing": ""
},
"iconicElements": {
"iconicMotifs": [],
"institutionsOrArtifacts": [],
"hardRules": []
}
"worldPromise": "",
"playerFantasy": "",
"themeBoundary": "",
"playerEntryPoint": "",
"coreConflict": "",
"keyRelationships": "",
"hiddenLines": "",
"iconicElements": ""
}
}"#;
}
nextAnchorContent 的 8 个锚点每个都只能是一个字符串或 null不允许输出对象或数组。
请把每个锚点写成一段凝练中文:
- worldPromise 关注世界钩子、差异点、玩家体验。
- playerFantasy 关注玩家身份、核心追求、失去风险。
- themeBoundary 关注主题气质、美术方向、禁用方向。
- playerEntryPoint 关注开局身份、开局问题、行动动机。
- coreConflict 关注表层冲突、隐藏危机、首次触发点。
- keyRelationships 关注关键人物关系、关系类型、代价或秘密。
- hiddenLines 关注隐藏真相、误导线索、揭示节奏。
- iconicElements 关注标志意象、组织/物件、硬规则。
"#;
pub(crate) fn render_dynamic_state_context(dynamic_state: &PromptDynamicState) -> String {
format!(

View File

@@ -1,27 +1,13 @@
/// 自定义世界角色主图提示词脚本。
pub(crate) fn build_character_visual_prompt(
prompt_text: &str,
character_brief_text: Option<&str>,
) -> String {
let character_brief = [character_brief_text.unwrap_or_default(), prompt_text]
.into_iter()
.map(str::trim)
.filter(|value| !value.is_empty())
.collect::<Vec<_>>()
.join("\n");
build_master_prompt(character_brief.as_str())
/// 自定义世界角色主图提示词脚本。
pub(crate) fn build_character_visual_prompt(prompt_text: &str) -> String {
build_master_prompt(prompt_text.trim())
}
/// 角色主图被供应商内容审核拦截时使用的安全兜底提示词。
///
/// 这里刻意不继续携带角色姓名、作品名和长设定文本,避免把可疑专名原样送回上游导致连续失败。
pub(crate) fn build_fallback_moderation_safe_character_visual_prompt(
prompt_text: &str,
character_brief_text: Option<&str>,
) -> String {
let source = [character_brief_text.unwrap_or_default(), prompt_text].join(" ");
let archetype = resolve_original_role_archetype(source.as_str());
pub(crate) fn build_fallback_moderation_safe_character_visual_prompt(prompt_text: &str) -> String {
let archetype = resolve_original_role_archetype(prompt_text);
build_master_prompt(
[
@@ -61,11 +47,11 @@ fn resolve_original_role_archetype(source: &str) -> &'static str {
/// 角色主图统一提示词骨架,迁移自旧共享 qwenSprite 主链。
fn build_master_prompt(character_brief: &str) -> String {
[
"单人2D 横版游戏角色标准设定图,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,细节精致,设计感足,适合后续制作 sprite sheet 动画。".to_string(),
"单人2D像素角色形象,头身比必须控制在 1.5 到 2 头身,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。".to_string(),
"视角要求:角色采用横版动作素材常用的右向斜侧身站姿,身体整体朝右,但保留少量正面信息,能读到面部轮廓与胸肩结构,不是完全 90 度纯右视图,也不是正面立绘。".to_string(),
"主体要求:画面中只保留单个角色主体,不要额外人物、动物、召唤物、载具或陪体。".to_string(),
"画面要求1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。".to_string(),
"风格要求:横版像素角色,头身比必须控制在 1 到 1.5 头身。使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点。".to_string(),
"风格要求:横版像素角色,细节精致,设计感足。使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点。".to_string(),
"如果角色形象设定没有明确要求非人身体结构,默认优先使用人类或类人动作角色骨架。\
默认将角色形象设定作用在角色自身的服装剪裁、材质、纹样、饰品、发光细节上。".to_string(),
"角色形象设定:".to_string(),
@@ -103,8 +89,6 @@ pub(crate) fn build_character_visual_negative_prompt() -> String {
"文字",
"水印",
"UI 元素",
"软萌 Q版大头贴",
"儿童绘本风",
"厚涂插画感",
"低对比柔边",
]

View File

@@ -36,8 +36,8 @@ pub(crate) fn build_custom_world_framework_prompt(setting_text: &str) -> String
"- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。".to_string(),
"- camp 必须表示玩家开局时的落脚处,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。".to_string(),
"- camp.sceneTaskDescription 必须描述玩家首次进入开局场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。".to_string(),
"- camp.actBackgroundPromptTexts 必须恰好 3 条,分别对应开局场景第 1/2/3 幕背景图画面内容描述;每条都必须可直接交给生图模型,控制在 4090 个汉字内。".to_string(),
"- camp.actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(),
"- camp.actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;第 1 幕负责铺垫,第 2 幕必须让冲突升级,第 3 幕必须形成高潮或关键抉择;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(),
"- camp.actBackgroundPromptTexts 必须恰好 3 条,分别对应第 1/2/3 幕背景图画面内容描述;每条必须基于同序号 actEventDescriptions 和相关角色写出画面主体、站位空间、冲突痕迹与氛围,能直接交给生图模型,控制在 4090 个汉字内。".to_string(),
"- 不要输出 playableNpcs、storyNpcs、landmarks、items也不要输出任何角色和地图细节。".to_string(),
"- majorFactions 保持 2 到 3 个coreConflicts 保持 2 到 3 个。".to_string(),
"- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。".to_string(),
@@ -199,9 +199,9 @@ pub(crate) fn build_custom_world_landmark_seed_batch_prompt(
"- 每个地点只保留name、description、visualDescription、sceneTaskDescription、actBackgroundPromptTexts、actEventDescriptions。".to_string(),
"- sceneTaskDescription 必须描述玩家首次进入该场景时要完成的核心任务,会作为游戏章节任务生成上下文,控制在 24 到 56 个汉字内。".to_string(),
"- visualDescription 是打开场景背景图像生成面板时默认填入的场景描述,必须具体到画面主体、远近景层次、地面可站立区域和氛围识别点,控制在 32 到 80 个汉字内。".to_string(),
"- actBackgroundPromptTexts 必须恰好 3 条,分别对应这个场景章节的第 1/2/3 幕背景图画面内容描述;每条都必须是大模型根据当前地点、主线阶段和可出场角色直接写出的画面描述,控制在 4090 个汉字内。".to_string(),
"- actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;第 1 幕负责铺垫,第 2 幕必须让冲突升级,第 3 幕必须形成高潮或关键抉择;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(),
"- actBackgroundPromptTexts 必须恰好 3 条,分别对应这个场景章节的第 1/2/3 幕背景图画面内容描述;每条都必须基于同序号 actEventDescriptions、当前地点和可出场角色直接写出画面主体、站位空间、冲突痕迹与氛围控制在 40 到 90 个汉字内。".to_string(),
"- actBackgroundPromptTexts 禁止使用“某某第1幕背景玩家会在……”这类标题、摘要、规则句拼接格式必须像可直接交给生图模型的自然画面描述。".to_string(),
"- actEventDescriptions 必须恰好 3 条,分别描述每一幕发生的事件;事件必须和当前幕对面的角色强相关,控制在 24 到 56 个汉字内。".to_string(),
"- description 控制在 12 到 24 个汉字内。".to_string(),
"- 所有生成文本都必须使用中文。".to_string(),
"- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。".to_string(),
@@ -258,6 +258,7 @@ pub(crate) fn build_custom_world_landmark_network_batch_prompt(
"要求:".to_string(),
"- 必须只补全本批场景name 必须与本批场景完全一致,不得增删改名。".to_string(),
"- sceneNpcNames 只能引用上方可用场景角色名单中的名字,每个地点 1 到 3 个。".to_string(),
"- sceneNpcNames 的第一位会成为每幕对面主角色;三幕事件和幕背景必须围绕这个角色的行动、阻碍、试探或求助展开。".to_string(),
"- connectedLandmarkNames 优先引用已知关键场景名称,每个地点 1 到 3 个。".to_string(),
"- entryHook 控制在 16 到 36 个汉字内。".to_string(),
"- 所有生成文本都必须使用中文。".to_string(),

View File

@@ -47,13 +47,14 @@ use spacetime_client::{
PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput,
PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord,
PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord,
PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord,
PuzzleGeneratedImagesSaveRecordInput, PuzzlePublishRecordInput, PuzzleResultDraftRecord,
PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord,
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleMergedGroupRecord, PuzzlePieceStateRecord,
PuzzleRunDragRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput,
PuzzleRuntimeLevelRecord, PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord,
PuzzleWorkUpsertRecordInput, SpacetimeClientError,
PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord,
PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput,
PuzzleMergedGroupRecord, PuzzlePieceStateRecord, PuzzlePublishRecordInput,
PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord,
PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunRecord,
PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord,
PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput,
SpacetimeClientError,
};
use std::convert::Infallible;
use tokio::time::sleep;
@@ -1639,7 +1640,10 @@ async fn generate_puzzle_image_candidates(
let mut items = Vec::with_capacity(generated.images.len());
for (index, image) in generated.images.into_iter().enumerate() {
let candidate_id = format!("{session_id}-candidate-{}", candidate_start_index + index + 1);
let candidate_id = format!(
"{session_id}-candidate-{}",
candidate_start_index + index + 1
);
let asset = persist_puzzle_generated_asset(
state,
owner_user_id,
@@ -1690,10 +1694,12 @@ async fn build_local_next_puzzle_run(
}))
})?;
if current_level.status != "cleared" {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "current level is not cleared",
})));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "current level is not cleared",
})),
);
}
if let Some(gallery_item) = resolve_gallery_next_puzzle_work(state, &run).await? {
@@ -1702,10 +1708,12 @@ async fn build_local_next_puzzle_run(
let source_session_id = payload.source_session_id.unwrap_or_default();
if source_session_id.trim().is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "sourceSessionId is required when gallery has no next puzzle work",
})));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "sourceSessionId is required when gallery has no next puzzle work",
})),
);
}
let session = state
.spacetime_client()
@@ -1767,14 +1775,23 @@ async fn build_local_next_puzzle_run(
let candidate = updated_session
.draft
.as_ref()
.and_then(|draft| draft.candidates.iter().find(|candidate| !candidate.image_src.is_empty()))
.and_then(|draft| {
draft
.candidates
.iter()
.find(|candidate| !candidate.image_src.is_empty())
})
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": PUZZLE_RUNTIME_PROVIDER,
"message": "现场生成后没有可用候选图",
}))
})?;
Ok(build_next_run_from_candidate(run, &updated_session, candidate))
Ok(build_next_run_from_candidate(
run,
&updated_session,
candidate,
))
}
async fn resolve_gallery_next_puzzle_work(
@@ -1788,7 +1805,10 @@ async fn resolve_gallery_next_puzzle_work(
.map_err(map_puzzle_client_error)?;
Ok(items.into_iter().find(|item| {
item.publication_status == "published"
&& item.cover_image_src.as_ref().is_some_and(|value| !value.is_empty())
&& item
.cover_image_src
.as_ref()
.is_some_and(|value| !value.is_empty())
&& !run.played_profile_ids.contains(&item.profile_id)
}))
}
@@ -1836,7 +1856,9 @@ fn build_next_run_from_candidate(
.map(|draft| format!("{} · 候选 {}", draft.level_name, level_index))
.unwrap_or_else(|| format!("候选拼图 {level_index}")),
"当前草稿".to_string(),
draft.map(|draft| draft.theme_tags.clone()).unwrap_or_default(),
draft
.map(|draft| draft.theme_tags.clone())
.unwrap_or_default(),
Some(candidate.image_src.clone()),
)
}
@@ -1893,13 +1915,14 @@ fn build_local_puzzle_board(grid_size: u32) -> PuzzleBoardRecord {
}
let pieces = (0..total)
.map(|index| {
let current = positions
.get(index as usize)
.cloned()
.unwrap_or(PuzzleCellPositionRecord {
row: index / grid_size,
col: index % grid_size,
});
let current =
positions
.get(index as usize)
.cloned()
.unwrap_or(PuzzleCellPositionRecord {
row: index / grid_size,
col: index % grid_size,
});
PuzzlePieceStateRecord {
piece_id: format!("piece-{index}"),
correct_row: index / grid_size,

View File

@@ -71,7 +71,7 @@ pub async fn stream_runtime_npc_chat_turn(
let llm_result =
generate_llm_npc_chat_turn(&state, &request_context, &payload, &npc_name).await;
let (mut body, npc_reply, suggestions) = match llm_result {
let (mut body, npc_reply, suggestions, function_suggestions, force_exit) = match llm_result {
Some(result) => result,
None => {
let npc_reply = build_deterministic_npc_reply(
@@ -79,11 +79,21 @@ pub async fn stream_runtime_npc_chat_turn(
player_message,
payload.npc_initiates_conversation,
);
let suggestions = if should_force_chat_exit(payload.chat_directive.as_ref()) {
let force_exit = should_force_chat_exit(payload.chat_directive.as_ref())
|| should_hostile_chat_breakoff_deterministically(
player_message,
payload.chat_directive.as_ref(),
);
let suggestions = if force_exit {
Vec::new()
} else {
build_deterministic_chat_suggestions(npc_name.as_str(), player_message)
};
let function_suggestions = if force_exit {
Vec::new()
} else {
build_fallback_function_suggestions(payload.chat_directive.as_ref())
};
let mut body = String::new();
append_sse_event(
&request_context,
@@ -91,7 +101,13 @@ pub async fn stream_runtime_npc_chat_turn(
"reply_delta",
&json!({ "text": npc_reply }),
)?;
(body, npc_reply, suggestions)
(
body,
npc_reply,
suggestions,
function_suggestions,
force_exit,
)
}
};
@@ -103,8 +119,9 @@ pub async fn stream_runtime_npc_chat_turn(
"affinityDelta": affinity_delta,
"affinityText": describe_affinity_shift(affinity_delta),
"suggestions": suggestions,
"functionSuggestions": function_suggestions,
"pendingQuestOffer": null,
"chatDirective": build_completion_directive(payload.chat_directive.as_ref()),
"chatDirective": build_completion_directive(payload.chat_directive.as_ref(), force_exit),
});
append_sse_event(&request_context, &mut body, "complete", &complete_payload)?;
@@ -117,7 +134,7 @@ async fn generate_llm_npc_chat_turn(
request_context: &RequestContext,
payload: &NpcChatTurnRequest,
npc_name: &str,
) -> Option<(String, String, Vec<String>)> {
) -> Option<(String, String, Vec<String>, Vec<Value>, bool)> {
let llm_client = state.llm_client()?;
let character = payload
.character
@@ -169,7 +186,7 @@ async fn generate_llm_npc_chat_turn(
});
if should_force_chat_exit(payload.chat_directive.as_ref()) {
return Some((body, npc_reply, Vec::new()));
return Some((body, npc_reply, Vec::new(), Vec::new(), true));
}
let suggestion_prompt =
@@ -180,15 +197,37 @@ async fn generate_llm_npc_chat_turn(
]);
suggestion_request.max_tokens = Some(200);
suggestion_request.enable_web_search = state.config.rpg_llm_web_search_enabled;
let suggestions = llm_client
let suggestion_text = llm_client
.request_text(suggestion_request)
.await
.ok()
.map(|response| parse_line_list_content(response.content.as_str(), 3))
.filter(|items| items.len() == 3)
.unwrap_or_else(|| build_fallback_npc_chat_suggestions(payload.player_message.as_str()));
.map(|response| response.content)
.unwrap_or_default();
let (mut suggestions, mut function_suggestions, should_end_chat) =
parse_npc_chat_suggestion_resolution(
suggestion_text.as_str(),
payload.chat_directive.as_ref(),
);
let force_exit = should_end_chat
|| should_hostile_chat_breakoff_deterministically(
payload.player_message.as_str(),
payload.chat_directive.as_ref(),
);
Some((body, npc_reply, suggestions))
if force_exit {
suggestions.clear();
function_suggestions.clear();
} else if suggestions.is_empty() {
suggestions = build_fallback_npc_chat_suggestions(payload.player_message.as_str());
}
Some((
body,
npc_reply,
suggestions,
function_suggestions,
force_exit,
))
}
fn build_deterministic_npc_reply(
@@ -206,12 +245,12 @@ fn build_deterministic_npc_reply(
fn build_deterministic_chat_suggestions(npc_name: &str, player_message: &str) -> Vec<String> {
// 建议只承载玩家可点选的行动意图,不在 UI 里额外塞说明文案。
vec![
format!("继续询问{npc_name}的近况"),
"追问这里发生了什么".to_string(),
format!("{npc_name},我想先听你说"),
"这件事哪里不对劲".to_string(),
if player_message.contains('帮') || player_message.contains('忙') {
"请对方说清需要什么帮助".to_string()
"先别绕,说清代价".to_string()
} else {
"换个轻松的话题".to_string()
"你是不是还瞒着我".to_string()
},
]
}
@@ -225,33 +264,164 @@ fn build_fallback_npc_chat_suggestions(player_message: &str) -> Vec<String> {
};
vec![
"你刚才那句是什么意思".to_string(),
"我愿意先听你说完".to_string(),
format!("这事和{topic}有关吗"),
"愿意再说清楚点吗".to_string(),
"别再避重就轻".to_string(),
]
}
fn build_completion_directive(chat_directive: Option<&Value>) -> Value {
fn build_fallback_function_suggestions(chat_directive: Option<&Value>) -> Vec<Value> {
read_function_options(chat_directive)
.into_iter()
.filter(|option| {
read_string_field(option, "functionId")
.as_deref()
.is_some_and(|function_id| function_id != "npc_chat")
})
.take(2)
.filter_map(|option| {
let function_id = read_string_field(option, "functionId")?;
let action_text = read_string_field(option, "actionText")?;
Some(json!({
"functionId": function_id,
"actionText": action_text,
}))
})
.collect()
}
fn build_completion_directive(chat_directive: Option<&Value>, force_exit: bool) -> Value {
let Some(directive) = chat_directive else {
return Value::Null;
};
let closing_mode = read_string_field(directive, "closingMode")
.filter(|value| value == "foreshadow_close")
.unwrap_or_else(|| "free".to_string());
let force_exit = closing_mode == "foreshadow_close"
let force_exit = force_exit
|| closing_mode == "foreshadow_close"
|| directive
.get("forceExitAfterTurn")
.and_then(Value::as_bool)
.unwrap_or(false);
let termination_reason = if force_exit {
read_string_field(directive, "terminationReason")
.filter(|value| value == "player_exit" || value == "hostile_breakoff")
.or_else(|| {
if is_hostile_model_chat(chat_directive) {
Some("hostile_breakoff".to_string())
} else {
None
}
})
} else {
None
};
json!({
"turnLimit": directive.get("turnLimit").cloned().unwrap_or(Value::Null),
"remainingTurns": directive.get("remainingTurns").cloned().unwrap_or(Value::Null),
"forceExit": force_exit,
"closingMode": closing_mode,
"closingMode": if force_exit { "foreshadow_close" } else { closing_mode.as_str() },
"terminationReason": termination_reason,
})
}
fn parse_npc_chat_suggestion_resolution(
text: &str,
chat_directive: Option<&Value>,
) -> (Vec<String>, Vec<Value>, bool) {
let normalized = text.trim();
if normalized.is_empty() {
return (
Vec::new(),
build_fallback_function_suggestions(chat_directive),
false,
);
}
if let Ok(value) = serde_json::from_str::<Value>(normalized) {
let should_end_chat = value
.get("shouldEndChat")
.and_then(Value::as_bool)
.unwrap_or(false)
&& is_hostile_model_chat(chat_directive);
let suggestions = value
.get("suggestions")
.and_then(Value::as_array)
.map(|items| {
items
.iter()
.filter_map(Value::as_str)
.map(str::trim)
.filter(|item| !item.is_empty())
.map(ToOwned::to_owned)
.take(3)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let function_suggestions =
parse_function_suggestions(value.get("functionSuggestions"), chat_directive);
return (suggestions, function_suggestions, should_end_chat);
}
(
parse_line_list_content(normalized, 3),
build_fallback_function_suggestions(chat_directive),
false,
)
}
fn parse_function_suggestions(value: Option<&Value>, chat_directive: Option<&Value>) -> Vec<Value> {
let allowed_options = read_function_options(chat_directive);
let allowed_ids = allowed_options
.iter()
.filter_map(|item| read_string_field(item, "functionId"))
.collect::<Vec<_>>();
let mut used_ids: Vec<String> = Vec::new();
value
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|item| {
let function_id = read_string_field(item, "functionId")?;
if function_id == "npc_chat" {
return None;
}
if !allowed_ids.is_empty() && !allowed_ids.contains(&function_id) {
return None;
}
if used_ids.contains(&function_id) {
return None;
}
let fallback_text = allowed_options
.iter()
.find(|option| {
read_string_field(option, "functionId").as_deref() == Some(function_id.as_str())
})
.and_then(|option| read_string_field(option, "actionText"));
let action_text = read_string_field(item, "actionText")
.or(fallback_text)
.filter(|text| !text.trim().is_empty())?;
used_ids.push(function_id.clone());
Some(json!({
"functionId": function_id,
"actionText": action_text,
}))
})
.take(3)
.collect()
}
fn read_function_options(chat_directive: Option<&Value>) -> Vec<&Value> {
chat_directive
.and_then(|directive| directive.get("functionOptions"))
.and_then(Value::as_array)
.map(|items| items.iter().collect::<Vec<_>>())
.unwrap_or_default()
}
fn read_string_field(value: &Value, field: &str) -> Option<String> {
value
.get(field)
@@ -268,18 +438,61 @@ fn read_number_field(value: &Value, field: &str) -> Option<f64> {
.filter(|number| number.is_finite())
}
fn read_bool_field(value: &Value, field: &str) -> Option<bool> {
value.get(field).and_then(Value::as_bool)
}
fn should_force_chat_exit(chat_directive: Option<&Value>) -> bool {
let Some(directive) = chat_directive else {
return false;
};
read_string_field(directive, "closingMode").as_deref() == Some("foreshadow_close")
|| read_string_field(directive, "terminationReason").as_deref() == Some("player_exit")
|| directive
.get("forceExitAfterTurn")
.and_then(Value::as_bool)
.unwrap_or(false)
}
fn is_hostile_model_chat(chat_directive: Option<&Value>) -> bool {
let Some(directive) = chat_directive else {
return false;
};
read_string_field(directive, "terminationMode").as_deref() == Some("hostile_model")
|| read_bool_field(directive, "isHostileChat").unwrap_or(false)
}
fn should_hostile_chat_breakoff_deterministically(
player_message: &str,
chat_directive: Option<&Value>,
) -> bool {
if !is_hostile_model_chat(chat_directive) {
return false;
}
let Some(directive) = chat_directive else {
return false;
};
if read_string_field(directive, "terminationReason").as_deref() == Some("player_exit") {
return true;
}
let hostile_break_words = [
"动手",
"开战",
"拔刀",
"",
"",
"闭嘴",
"少废话",
"别挡路",
];
count_keyword_matches(player_message, &hostile_break_words) > 0
}
fn normalize_required_text(value: &str) -> Option<String> {
let normalized = value.trim();
if normalized.is_empty() {

View File

@@ -6,10 +6,16 @@ pub(crate) const NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT: &str = r#"你是角色扮演
- 如果这是第一次真正接触中的首轮回复,第一句必须先用自然招呼或开场判断起手,不能写成第三人称占位旁白。
回复长度控制在 1 到 3 句,必须紧接玩家刚说的话,自然推进气氛、情报或关系。"#;
pub(crate) const NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT: &str = r#"你要为玩家生成下一轮可直接点击的 3 条聊天续写候选
只输出纯文本,共 3 行,每行 1 条
不要加编号、项目符号、Markdown、JSON 或额外说明。
三条候选必须明显不同,分别体现继续追问、表达态度、轻微拉近关系这三种不同方向。"#;
pub(crate) const NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT: &str = r#"你要为 RPG NPC 聊天生成下一步候选,并判断敌对聊天是否已经收束
只输出 JSON不要输出 Markdown 或解释
JSON 结构:
{"shouldEndChat":false,"terminationReason":null,"suggestions":["温和共情台词","冷静追问台词","施压质疑台词"],"functionSuggestions":[{"functionId":"...","actionText":"玩家动作文本"}]}
- suggestions 是玩家下一轮可直接说出口的中文短句,每条 20 字以内;三条必须按顺序导向不同氛围和好感结果。
- suggestions 第 1 条温和共情,通常让气氛缓和、好感上升;第 2 条冷静追问或试探,通常保持中性但推进情报;第 3 条施压、质疑或立场冲突,通常让气氛变紧、好感下降或付出代价。
- functionSuggestions 只能从用户提示提供的 functionOptions 中挑选,不要发明 functionId。
- functionSuggestions 的 actionText 必须像玩家可点击动作,不暴露 functionId不写规则说明。
- 非敌对聊天 shouldEndChat 必须为 false。
- 敌对聊天可以随时 shouldEndChat=true且敌对 NPC 更偏好在话不投机、被威胁、玩家退出、底线被触碰时结束聊天。"#;
#[derive(Debug)]
pub(crate) struct NpcChatTurnPromptInput<'a> {
@@ -71,6 +77,17 @@ pub(crate) fn build_npc_chat_turn_reply_prompt(payload: &NpcChatTurnPromptInput<
let closing_mode = chat_directive.and_then(|record| read_string(record.get("closingMode")));
let is_limited_negative_affinity_chat =
limit_reason.as_deref() == Some("negative_affinity") && turn_limit > 0.0;
let is_hostile_model_chat = chat_directive
.and_then(|record| read_string(record.get("terminationMode")))
.as_deref()
== Some("hostile_model")
|| chat_directive
.and_then(|record| read_bool(record.get("isHostileChat")))
.unwrap_or(false);
let is_player_exit_turn = chat_directive
.and_then(|record| read_string(record.get("terminationReason")))
.as_deref()
== Some("player_exit");
let is_foreshadow_close_turn = closing_mode.as_deref() == Some("foreshadow_close")
|| chat_directive
.and_then(|record| read_bool(record.get("forceExitAfterTurn")))
@@ -142,6 +159,21 @@ pub(crate) fn build_npc_chat_turn_reply_prompt(payload: &NpcChatTurnPromptInput<
} else {
None
},
if is_hostile_model_chat {
Some("当前是敌对或负好感聊天。对方不受固定回合限制,但随时可能不耐烦、结束谈话并把局势推向战斗或驱逐。".to_string())
} else {
None
},
if is_hostile_model_chat {
Some("敌对角色更偏好短促、戒备、带威胁的回应;如果玩家逼问、挑衅、退场或话题触到底线,回复应自然收束到对峙前一刻。".to_string())
} else {
None
},
if is_player_exit_turn {
Some("玩家正在主动结束这轮聊天。请对这个收束动作作出回应,并留下自然的下一步入口。回复后聊天会结束。".to_string())
} else {
None
},
if is_limited_negative_affinity_chat {
Some(format!(
"在你回复完这一轮之后,还剩 {} 轮可以继续聊。",
@@ -205,6 +237,22 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt(
payload.dialogue
};
let combat_context_block = payload.combat_context.and_then(describe_npc_combat_context);
let chat_directive = payload.chat_directive.and_then(as_record);
let is_hostile_model_chat = chat_directive
.and_then(|record| read_string(record.get("terminationMode")))
.as_deref()
== Some("hostile_model")
|| chat_directive
.and_then(|record| read_bool(record.get("isHostileChat")))
.unwrap_or(false);
let is_player_exit_turn = chat_directive
.and_then(|record| read_string(record.get("terminationReason")))
.as_deref()
== Some("player_exit");
let function_options_block = chat_directive
.and_then(|record| record.get("functionOptions"))
.map(describe_function_options)
.filter(|text| !text.trim().is_empty());
[
Some(build_npc_dialogue_prompt_base(payload)),
@@ -213,11 +261,22 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt(
encounter.npc_name.as_str(),
)),
combat_context_block,
function_options_block,
Some(format!("玩家刚刚说:{}", payload.player_message)),
Some(format!("NPC 刚刚回复:{npc_reply}")),
Some("请围绕刚刚这轮对话,为玩家生成 3 条下一轮可以直接说出口的中文接话短句。".to_string()),
Some("每条都必须像玩家台词,不能写成行为描述、语气说明或策略建议".to_string()),
Some("每条都必须控制在 20 个字以内,不要加序号、引号、括号或解释。".to_string()),
if is_hostile_model_chat {
Some("这是敌对或负好感聊天。你需要判断这轮是否应该结束聊天;敌对角色更偏好随时终止并转入对峙".to_string())
} else {
Some("这是非敌对聊天shouldEndChat 必须为 false。".to_string())
},
if is_player_exit_turn {
Some("玩家已经选择结束聊天shouldEndChat 必须为 trueterminationReason 必须为 player_exit。".to_string())
} else {
None
},
Some("suggestions 必须按顺序生成三种明显不同的玩家台词:温和共情、冷静追问或试探、施压质疑;不要给出同一种态度的近义句。".to_string()),
Some("functionSuggestions 从 functionOptions 中挑可触发动作并改写 actionText。".to_string()),
Some("只输出 JSON{\"shouldEndChat\":false,\"terminationReason\":null,\"suggestions\":[\"...\"],\"functionSuggestions\":[{\"functionId\":\"...\",\"actionText\":\"...\"}]}".to_string()),
]
.into_iter()
.flatten()
@@ -226,6 +285,38 @@ pub(crate) fn build_npc_chat_turn_suggestion_prompt(
.join("\n\n")
}
fn describe_function_options(value: &Value) -> String {
let lines = value
.as_array()
.map(|items| {
items
.iter()
.take(8)
.filter_map(|item| {
let record = as_record(item)?;
let function_id = read_string(record.get("functionId"))?;
let action_text = read_string(record.get("actionText"))?;
let detail_text = read_string(record.get("detailText"));
let action = read_string(record.get("action"));
Some(format!(
"- functionId: {function_id}; actionText: {action_text}; action: {}; detail: {}",
action.unwrap_or_else(|| "unknown".to_string()),
detail_text.unwrap_or_else(|| "".to_string()),
))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if lines.is_empty() {
return String::new();
}
let mut result = vec!["当前聊天中可改写为动作候选的 functionOptions".to_string()];
result.extend(lines);
result.join("\n")
}
fn build_npc_dialogue_prompt_base(payload: &NpcChatTurnPromptInput<'_>) -> String {
let encounter = describe_encounter(payload.encounter);

View File

@@ -7,15 +7,18 @@ use axum::{
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileMembershipBenefitRecord,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileRechargeProductRecord,
RuntimeProfileRechargeProductRecord, RuntimeReferralInviteCenterRecord,
RuntimeReferralRedeemRecord,
};
use serde_json::{Value, json};
use shared_contracts::runtime::{
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
ProfileDashboardSummaryResponse, ProfileMembershipBenefitResponse, ProfileMembershipResponse,
ProfilePlayStatsResponse, ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse,
ProfileRechargeOrderResponse, ProfileRechargeProductResponse, ProfileWalletLedgerEntryResponse,
ProfileWalletLedgerResponse,
ProfileRechargeOrderResponse, ProfileRechargeProductResponse,
ProfileReferralInviteCenterResponse, ProfileWalletLedgerEntryResponse,
ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest,
RedeemProfileReferralInviteCodeResponse,
};
use spacetime_client::SpacetimeClientError;
use time::OffsetDateTime;
@@ -146,6 +149,54 @@ pub async fn create_profile_recharge_order(
))
}
pub async fn get_profile_referral_invite_center(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
let user_id = authenticated.claims().user_id().to_string();
let record = state
.spacetime_client()
.get_profile_referral_invite_center(user_id)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_referral_invite_center_response(record),
))
}
pub async fn redeem_profile_referral_invite_code(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<RedeemProfileReferralInviteCodeRequest>,
) -> Result<Json<Value>, Response> {
let user_id = authenticated.claims().user_id().to_string();
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
.redeem_profile_referral_invite_code(user_id, payload.invite_code, updated_at_micros as i64)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_redeem_profile_referral_invite_code_response(record),
))
}
pub async fn get_profile_play_stats(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -284,6 +335,36 @@ fn build_profile_recharge_order_response(
}
}
fn build_profile_referral_invite_center_response(
record: RuntimeReferralInviteCenterRecord,
) -> ProfileReferralInviteCenterResponse {
ProfileReferralInviteCenterResponse {
invite_code: record.invite_code,
invite_link_path: record.invite_link_path,
invited_count: record.invited_count,
rewarded_invite_count: record.rewarded_invite_count,
today_inviter_reward_count: record.today_inviter_reward_count,
today_inviter_reward_remaining: record.today_inviter_reward_remaining,
reward_points: record.reward_points,
has_redeemed_code: record.has_redeemed_code,
bound_inviter_user_id: record.bound_inviter_user_id,
bound_at: record.bound_at,
updated_at: record.updated_at,
}
}
fn build_redeem_profile_referral_invite_code_response(
record: RuntimeReferralRedeemRecord,
) -> RedeemProfileReferralInviteCodeResponse {
RedeemProfileReferralInviteCodeResponse {
center: build_profile_referral_invite_center_response(record.center),
invitee_reward_granted: record.invitee_reward_granted,
inviter_reward_granted: record.inviter_reward_granted,
invitee_balance_after: record.invitee_balance_after,
inviter_balance_after: record.inviter_balance_after,
}
}
#[cfg(test)]
mod tests {
use axum::{
@@ -391,6 +472,43 @@ mod tests {
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn profile_referral_invite_center_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("GET")
.uri("/api/profile/referrals/invite-center")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn profile_referral_redeem_code_requires_authentication() {
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/profile/referrals/redeem-code")
.header("content-type", "application/json")
.body(Body::from(r#"{"inviteCode":"SY12345678"}"#))
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn profile_dashboard_compat_route_matches_main_route_error_shape() {
assert_compat_route_matches_main_route_error_shape(
@@ -479,16 +597,7 @@ mod tests {
}
async fn seed_authenticated_state() -> AppState {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.password_entry_service()
.execute(module_auth::PasswordEntryInput {
username: "runtime_profile_user".to_string(),
password: "secret123".to_string(),
})
.await
.expect("seed login should succeed");
state
AppState::new(AppConfig::default()).expect("state should build")
}
fn issue_access_token(state: &AppState) -> String {

View File

@@ -1418,7 +1418,7 @@ pub fn build_custom_world_published_profile_compile_snapshot(
}
pub fn empty_agent_anchor_content_json() -> String {
r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":[],"hiddenLines":null,"iconicElements":null}"#.to_string()
r#"{"worldPromise":null,"playerFantasy":null,"themeBoundary":null,"playerEntryPoint":null,"coreConflict":null,"keyRelationships":null,"hiddenLines":null,"iconicElements":null}"#.to_string()
}
pub fn empty_agent_creator_intent_readiness_json() -> String {

View File

@@ -99,8 +99,6 @@ pub struct CharacterVisualGenerateRequest {
pub source_mode: CharacterVisualSourceMode,
pub prompt_text: String,
#[serde(default)]
pub character_brief_text: Option<String>,
#[serde(default)]
pub reference_image_data_urls: Vec<String>,
pub candidate_count: u32,
pub image_model: String,

View File

@@ -5,6 +5,8 @@ pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark";
pub const SAVE_SNAPSHOT_VERSION: u32 = 2;
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
@@ -220,6 +222,38 @@ pub struct CreateProfileRechargeOrderResponse {
pub center: ProfileRechargeCenterResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileReferralInviteCenterResponse {
pub invite_code: String,
pub invite_link_path: String,
pub invited_count: u32,
pub rewarded_invite_count: u32,
pub today_inviter_reward_count: u32,
pub today_inviter_reward_remaining: u32,
pub reward_points: u64,
pub has_redeemed_code: bool,
pub bound_inviter_user_id: Option<String>,
pub bound_at: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileReferralInviteCodeRequest {
pub invite_code: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RedeemProfileReferralInviteCodeResponse {
pub center: ProfileReferralInviteCenterResponse,
pub invitee_reward_granted: bool,
pub inviter_reward_granted: bool,
pub invitee_balance_after: u64,
pub inviter_balance_after: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfilePlayedWorkSummaryResponse {

View File

@@ -121,7 +121,8 @@ use module_runtime::{
RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme,
RuntimeProfileDashboardRecord, RuntimeProfilePlayStatsRecord,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileSaveArchiveRecord, RuntimeProfileWalletLedgerEntryRecord, RuntimeSettingsRecord,
RuntimeProfileSaveArchiveRecord, RuntimeProfileWalletLedgerEntryRecord,
RuntimeReferralInviteCenterRecord, RuntimeReferralRedeemRecord, RuntimeSettingsRecord,
RuntimeSnapshotRecord, build_runtime_browse_history_clear_input,
build_runtime_browse_history_list_input, build_runtime_browse_history_record,
build_runtime_browse_history_sync_input, build_runtime_profile_dashboard_get_input,
@@ -132,7 +133,9 @@ use module_runtime::{
build_runtime_profile_save_archive_list_input, build_runtime_profile_save_archive_record,
build_runtime_profile_save_archive_resume_input,
build_runtime_profile_wallet_ledger_entry_record,
build_runtime_profile_wallet_ledger_list_input, build_runtime_setting_get_input,
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
build_runtime_referral_redeem_record, build_runtime_setting_get_input,
build_runtime_setting_record, build_runtime_setting_upsert_input,
build_runtime_snapshot_delete_input, build_runtime_snapshot_get_input,
build_runtime_snapshot_record, build_runtime_snapshot_upsert_input,

View File

@@ -139,6 +139,26 @@ impl From<module_runtime::RuntimeProfileRechargeOrderCreateInput>
}
}
impl From<module_runtime::RuntimeReferralInviteCenterGetInput>
for RuntimeReferralInviteCenterGetInput
{
fn from(input: module_runtime::RuntimeReferralInviteCenterGetInput) -> Self {
Self {
user_id: input.user_id,
}
}
}
impl From<module_runtime::RuntimeReferralRedeemInput> for RuntimeReferralRedeemInput {
fn from(input: module_runtime::RuntimeReferralRedeemInput) -> Self {
Self {
user_id: input.user_id,
invite_code: input.invite_code,
updated_at_micros: input.updated_at_micros,
}
}
}
impl From<module_runtime::RuntimeProfilePlayStatsGetInput> for RuntimeProfilePlayStatsGetInput {
fn from(input: module_runtime::RuntimeProfilePlayStatsGetInput) -> Self {
Self {
@@ -675,6 +695,50 @@ pub(crate) fn map_runtime_profile_recharge_order_procedure_result(
))
}
pub(crate) fn map_runtime_referral_invite_center_procedure_result(
result: RuntimeReferralInviteCenterProcedureResult,
) -> Result<RuntimeReferralInviteCenterRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 referral invite center 快照".to_string(),
)
})?;
Ok(build_runtime_referral_invite_center_record(
map_runtime_referral_invite_center_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_referral_redeem_procedure_result(
result: RuntimeReferralRedeemProcedureResult,
) -> Result<RuntimeReferralRedeemRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
}
let snapshot = result.record.ok_or_else(|| {
SpacetimeClientError::Procedure(
"SpacetimeDB procedure 未返回 referral redeem 快照".to_string(),
)
})?;
Ok(build_runtime_referral_redeem_record(
map_runtime_referral_redeem_snapshot(snapshot),
))
}
pub(crate) fn map_runtime_profile_play_stats_procedure_result(
result: RuntimeProfilePlayStatsProcedureResult,
) -> Result<RuntimeProfilePlayStatsRecord, SpacetimeClientError> {
@@ -1513,6 +1577,37 @@ pub(crate) fn map_runtime_profile_recharge_order_snapshot(
}
}
pub(crate) fn map_runtime_referral_invite_center_snapshot(
snapshot: RuntimeReferralInviteCenterSnapshot,
) -> module_runtime::RuntimeReferralInviteCenterSnapshot {
module_runtime::RuntimeReferralInviteCenterSnapshot {
user_id: snapshot.user_id,
invite_code: snapshot.invite_code,
invite_link_path: snapshot.invite_link_path,
invited_count: snapshot.invited_count,
rewarded_invite_count: snapshot.rewarded_invite_count,
today_inviter_reward_count: snapshot.today_inviter_reward_count,
today_inviter_reward_remaining: snapshot.today_inviter_reward_remaining,
reward_points: snapshot.reward_points,
has_redeemed_code: snapshot.has_redeemed_code,
bound_inviter_user_id: snapshot.bound_inviter_user_id,
bound_at_micros: snapshot.bound_at_micros,
updated_at_micros: snapshot.updated_at_micros,
}
}
pub(crate) fn map_runtime_referral_redeem_snapshot(
snapshot: RuntimeReferralRedeemSnapshot,
) -> module_runtime::RuntimeReferralRedeemSnapshot {
module_runtime::RuntimeReferralRedeemSnapshot {
center: map_runtime_referral_invite_center_snapshot(snapshot.center),
invitee_reward_granted: snapshot.invitee_reward_granted,
inviter_reward_granted: snapshot.inviter_reward_granted,
invitee_balance_after: snapshot.invitee_balance_after,
inviter_balance_after: snapshot.inviter_balance_after,
}
}
pub(crate) fn map_runtime_profile_played_world_snapshot(
snapshot: RuntimeProfilePlayedWorldSnapshot,
) -> module_runtime::RuntimeProfilePlayedWorldSnapshot {

View File

@@ -0,0 +1,58 @@
// 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_referral_invite_center_get_input_type::RuntimeReferralInviteCenterGetInput;
use super::runtime_referral_invite_center_procedure_result_type::RuntimeReferralInviteCenterProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetProfileReferralInviteCenterArgs {
pub input: RuntimeReferralInviteCenterGetInput,
}
impl __sdk::InModule for GetProfileReferralInviteCenterArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_profile_referral_invite_center`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_profile_referral_invite_center {
fn get_profile_referral_invite_center(&self, input: RuntimeReferralInviteCenterGetInput,
) {
self.get_profile_referral_invite_center_then(input, |_, _| {});
}
fn get_profile_referral_invite_center_then(
&self,
input: RuntimeReferralInviteCenterGetInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeReferralInviteCenterProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
impl get_profile_referral_invite_center for super::RemoteProcedures {
fn get_profile_referral_invite_center_then(
&self,
input: RuntimeReferralInviteCenterGetInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeReferralInviteCenterProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeReferralInviteCenterProcedureResult>(
"get_profile_referral_invite_center",
GetProfileReferralInviteCenterArgs { input, },
__callback,
);
}
}

View File

@@ -189,9 +189,11 @@ pub mod player_progression_grant_source_type;
pub mod player_progression_procedure_result_type;
pub mod player_progression_snapshot_type;
pub mod profile_dashboard_state_type;
pub mod profile_invite_code_type;
pub mod profile_membership_type;
pub mod profile_played_world_type;
pub mod profile_recharge_order_type;
pub mod profile_referral_relation_type;
pub mod profile_save_archive_type;
pub mod profile_wallet_ledger_type;
pub mod puzzle_agent_message_finalize_input_type;
@@ -305,6 +307,12 @@ pub mod runtime_profile_wallet_ledger_entry_snapshot_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_source_type_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_snapshot_type;
pub mod runtime_referral_redeem_input_type;
pub mod runtime_referral_redeem_procedure_result_type;
pub mod runtime_referral_redeem_snapshot_type;
pub mod runtime_setting_type;
pub mod runtime_setting_get_input_type;
pub mod runtime_setting_procedure_result_type;
@@ -359,7 +367,52 @@ pub mod unpublish_custom_world_profile_reducer;
pub mod upsert_chapter_progression_reducer;
pub mod upsert_custom_world_profile_reducer;
pub mod upsert_npc_state_reducer;
pub mod ai_result_reference_table;
pub mod ai_task_table;
pub mod ai_task_stage_table;
pub mod ai_text_chunk_table;
pub mod asset_entity_binding_table;
pub mod asset_object_table;
pub mod auth_identity_table;
pub mod auth_store_snapshot_table;
pub mod battle_state_table;
pub mod big_fish_agent_message_table;
pub mod big_fish_asset_slot_table;
pub mod big_fish_creation_session_table;
pub mod big_fish_runtime_run_table;
pub mod chapter_progression_table;
pub mod custom_world_agent_message_table;
pub mod custom_world_agent_operation_table;
pub mod custom_world_agent_session_table;
pub mod custom_world_draft_card_table;
pub mod custom_world_gallery_entry_table;
pub mod custom_world_profile_table;
pub mod custom_world_session_table;
pub mod inventory_slot_table;
pub mod npc_state_table;
pub mod player_progression_table;
pub mod profile_dashboard_state_table;
pub mod profile_invite_code_table;
pub mod profile_membership_table;
pub mod profile_played_world_table;
pub mod profile_recharge_order_table;
pub mod profile_referral_relation_table;
pub mod profile_save_archive_table;
pub mod profile_wallet_ledger_table;
pub mod puzzle_agent_message_table;
pub mod puzzle_agent_session_table;
pub mod puzzle_runtime_run_table;
pub mod puzzle_work_profile_table;
pub mod quest_log_table;
pub mod quest_record_table;
pub mod refresh_session_table;
pub mod runtime_setting_table;
pub mod runtime_snapshot_table;
pub mod story_event_table;
pub mod story_session_table;
pub mod treasure_record_table;
pub mod user_account_table;
pub mod user_browse_history_table;
pub mod advance_puzzle_next_level_procedure;
pub mod append_ai_text_chunk_and_return_procedure;
pub mod apply_chapter_progression_ledger_entry_and_return_procedure;
@@ -409,6 +462,7 @@ pub mod get_player_progression_or_default_procedure;
pub mod get_profile_dashboard_procedure;
pub mod get_profile_play_stats_procedure;
pub mod get_profile_recharge_center_procedure;
pub mod get_profile_referral_invite_center_procedure;
pub mod get_puzzle_agent_session_procedure;
pub mod get_puzzle_gallery_detail_procedure;
pub mod get_puzzle_run_procedure;
@@ -432,6 +486,7 @@ pub mod publish_big_fish_game_procedure;
pub mod publish_custom_world_profile_and_return_procedure;
pub mod publish_custom_world_world_procedure;
pub mod publish_puzzle_work_procedure;
pub mod redeem_profile_referral_invite_code_procedure;
pub mod resolve_combat_action_and_return_procedure;
pub mod resolve_npc_battle_interaction_and_return_procedure;
pub mod resolve_npc_interaction_and_return_procedure;
@@ -636,9 +691,11 @@ pub use player_progression_grant_source_type::PlayerProgressionGrantSource;
pub use player_progression_procedure_result_type::PlayerProgressionProcedureResult;
pub use player_progression_snapshot_type::PlayerProgressionSnapshot;
pub use profile_dashboard_state_type::ProfileDashboardState;
pub use profile_invite_code_type::ProfileInviteCode;
pub use profile_membership_type::ProfileMembership;
pub use profile_played_world_type::ProfilePlayedWorld;
pub use profile_recharge_order_type::ProfileRechargeOrder;
pub use profile_referral_relation_type::ProfileReferralRelation;
pub use profile_save_archive_type::ProfileSaveArchive;
pub use profile_wallet_ledger_type::ProfileWalletLedger;
pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInput;
@@ -752,6 +809,12 @@ 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_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult;
pub use runtime_profile_wallet_ledger_source_type_type::RuntimeProfileWalletLedgerSourceType;
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_snapshot_type::RuntimeReferralInviteCenterSnapshot;
pub use runtime_referral_redeem_input_type::RuntimeReferralRedeemInput;
pub use runtime_referral_redeem_procedure_result_type::RuntimeReferralRedeemProcedureResult;
pub use runtime_referral_redeem_snapshot_type::RuntimeReferralRedeemSnapshot;
pub use runtime_setting_type::RuntimeSetting;
pub use runtime_setting_get_input_type::RuntimeSettingGetInput;
pub use runtime_setting_procedure_result_type::RuntimeSettingProcedureResult;
@@ -782,7 +845,52 @@ pub use treasure_resolve_input_type::TreasureResolveInput;
pub use unequip_inventory_item_input_type::UnequipInventoryItemInput;
pub use user_account_type::UserAccount;
pub use user_browse_history_type::UserBrowseHistory;
pub use ai_result_reference_table::*;
pub use ai_task_table::*;
pub use ai_task_stage_table::*;
pub use ai_text_chunk_table::*;
pub use asset_entity_binding_table::*;
pub use asset_object_table::*;
pub use auth_identity_table::*;
pub use auth_store_snapshot_table::*;
pub use battle_state_table::*;
pub use big_fish_agent_message_table::*;
pub use big_fish_asset_slot_table::*;
pub use big_fish_creation_session_table::*;
pub use big_fish_runtime_run_table::*;
pub use chapter_progression_table::*;
pub use custom_world_agent_message_table::*;
pub use custom_world_agent_operation_table::*;
pub use custom_world_agent_session_table::*;
pub use custom_world_draft_card_table::*;
pub use custom_world_gallery_entry_table::*;
pub use custom_world_profile_table::*;
pub use custom_world_session_table::*;
pub use inventory_slot_table::*;
pub use npc_state_table::*;
pub use player_progression_table::*;
pub use profile_dashboard_state_table::*;
pub use profile_invite_code_table::*;
pub use profile_membership_table::*;
pub use profile_played_world_table::*;
pub use profile_recharge_order_table::*;
pub use profile_referral_relation_table::*;
pub use profile_save_archive_table::*;
pub use profile_wallet_ledger_table::*;
pub use puzzle_agent_message_table::*;
pub use puzzle_agent_session_table::*;
pub use puzzle_runtime_run_table::*;
pub use puzzle_work_profile_table::*;
pub use quest_log_table::*;
pub use quest_record_table::*;
pub use refresh_session_table::*;
pub use runtime_setting_table::*;
pub use runtime_snapshot_table::*;
pub use story_event_table::*;
pub use story_session_table::*;
pub use treasure_record_table::*;
pub use user_account_table::*;
pub use user_browse_history_table::*;
pub use accept_quest_reducer::accept_quest;
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry;
@@ -856,6 +964,7 @@ pub use get_player_progression_or_default_procedure::get_player_progression_or_d
pub use get_profile_dashboard_procedure::get_profile_dashboard;
pub use get_profile_play_stats_procedure::get_profile_play_stats;
pub use get_profile_recharge_center_procedure::get_profile_recharge_center;
pub use get_profile_referral_invite_center_procedure::get_profile_referral_invite_center;
pub use get_puzzle_agent_session_procedure::get_puzzle_agent_session;
pub use get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail;
pub use get_puzzle_run_procedure::get_puzzle_run;
@@ -879,6 +988,7 @@ pub use publish_big_fish_game_procedure::publish_big_fish_game;
pub use publish_custom_world_profile_and_return_procedure::publish_custom_world_profile_and_return;
pub use publish_custom_world_world_procedure::publish_custom_world_world;
pub use publish_puzzle_work_procedure::publish_puzzle_work;
pub use redeem_profile_referral_invite_code_procedure::redeem_profile_referral_invite_code;
pub use resolve_combat_action_and_return_procedure::resolve_combat_action_and_return;
pub use resolve_npc_battle_interaction_and_return_procedure::resolve_npc_battle_interaction_and_return;
pub use resolve_npc_interaction_and_return_procedure::resolve_npc_interaction_and_return;
@@ -1154,7 +1264,52 @@ fn args_bsatn(&self) -> Result<Vec<u8>, __sats::bsatn::EncodeError> {
#[allow(non_snake_case)]
#[doc(hidden)]
pub struct DbUpdate {
custom_world_gallery_entry: __sdk::TableUpdate<CustomWorldGalleryEntry>,
ai_result_reference: __sdk::TableUpdate<AiResultReference>,
ai_task: __sdk::TableUpdate<AiTask>,
ai_task_stage: __sdk::TableUpdate<AiTaskStage>,
ai_text_chunk: __sdk::TableUpdate<AiTextChunk>,
asset_entity_binding: __sdk::TableUpdate<AssetEntityBinding>,
asset_object: __sdk::TableUpdate<AssetObject>,
auth_identity: __sdk::TableUpdate<AuthIdentity>,
auth_store_snapshot: __sdk::TableUpdate<AuthStoreSnapshot>,
battle_state: __sdk::TableUpdate<BattleState>,
big_fish_agent_message: __sdk::TableUpdate<BigFishAgentMessage>,
big_fish_asset_slot: __sdk::TableUpdate<BigFishAssetSlot>,
big_fish_creation_session: __sdk::TableUpdate<BigFishCreationSession>,
big_fish_runtime_run: __sdk::TableUpdate<BigFishRuntimeRun>,
chapter_progression: __sdk::TableUpdate<ChapterProgression>,
custom_world_agent_message: __sdk::TableUpdate<CustomWorldAgentMessage>,
custom_world_agent_operation: __sdk::TableUpdate<CustomWorldAgentOperation>,
custom_world_agent_session: __sdk::TableUpdate<CustomWorldAgentSession>,
custom_world_draft_card: __sdk::TableUpdate<CustomWorldDraftCard>,
custom_world_gallery_entry: __sdk::TableUpdate<CustomWorldGalleryEntry>,
custom_world_profile: __sdk::TableUpdate<CustomWorldProfile>,
custom_world_session: __sdk::TableUpdate<CustomWorldSession>,
inventory_slot: __sdk::TableUpdate<InventorySlot>,
npc_state: __sdk::TableUpdate<NpcState>,
player_progression: __sdk::TableUpdate<PlayerProgression>,
profile_dashboard_state: __sdk::TableUpdate<ProfileDashboardState>,
profile_invite_code: __sdk::TableUpdate<ProfileInviteCode>,
profile_membership: __sdk::TableUpdate<ProfileMembership>,
profile_played_world: __sdk::TableUpdate<ProfilePlayedWorld>,
profile_recharge_order: __sdk::TableUpdate<ProfileRechargeOrder>,
profile_referral_relation: __sdk::TableUpdate<ProfileReferralRelation>,
profile_save_archive: __sdk::TableUpdate<ProfileSaveArchive>,
profile_wallet_ledger: __sdk::TableUpdate<ProfileWalletLedger>,
puzzle_agent_message: __sdk::TableUpdate<PuzzleAgentMessageRow>,
puzzle_agent_session: __sdk::TableUpdate<PuzzleAgentSessionRow>,
puzzle_runtime_run: __sdk::TableUpdate<PuzzleRuntimeRunRow>,
puzzle_work_profile: __sdk::TableUpdate<PuzzleWorkProfileRow>,
quest_log: __sdk::TableUpdate<QuestLog>,
quest_record: __sdk::TableUpdate<QuestRecord>,
refresh_session: __sdk::TableUpdate<RefreshSession>,
runtime_setting: __sdk::TableUpdate<RuntimeSetting>,
runtime_snapshot: __sdk::TableUpdate<RuntimeSnapshotRow>,
story_event: __sdk::TableUpdate<StoryEvent>,
story_session: __sdk::TableUpdate<StorySession>,
treasure_record: __sdk::TableUpdate<TreasureRecord>,
user_account: __sdk::TableUpdate<UserAccount>,
user_browse_history: __sdk::TableUpdate<UserBrowseHistory>,
}
@@ -1165,7 +1320,52 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
for table_update in __sdk::transaction_update_iter_table_updates(raw) {
match &table_update.table_name[..] {
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?),
"ai_result_reference" => db_update.ai_result_reference.append(ai_result_reference_table::parse_table_update(table_update)?),
"ai_task" => db_update.ai_task.append(ai_task_table::parse_table_update(table_update)?),
"ai_task_stage" => db_update.ai_task_stage.append(ai_task_stage_table::parse_table_update(table_update)?),
"ai_text_chunk" => db_update.ai_text_chunk.append(ai_text_chunk_table::parse_table_update(table_update)?),
"asset_entity_binding" => db_update.asset_entity_binding.append(asset_entity_binding_table::parse_table_update(table_update)?),
"asset_object" => db_update.asset_object.append(asset_object_table::parse_table_update(table_update)?),
"auth_identity" => db_update.auth_identity.append(auth_identity_table::parse_table_update(table_update)?),
"auth_store_snapshot" => db_update.auth_store_snapshot.append(auth_store_snapshot_table::parse_table_update(table_update)?),
"battle_state" => db_update.battle_state.append(battle_state_table::parse_table_update(table_update)?),
"big_fish_agent_message" => db_update.big_fish_agent_message.append(big_fish_agent_message_table::parse_table_update(table_update)?),
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(big_fish_asset_slot_table::parse_table_update(table_update)?),
"big_fish_creation_session" => db_update.big_fish_creation_session.append(big_fish_creation_session_table::parse_table_update(table_update)?),
"big_fish_runtime_run" => db_update.big_fish_runtime_run.append(big_fish_runtime_run_table::parse_table_update(table_update)?),
"chapter_progression" => db_update.chapter_progression.append(chapter_progression_table::parse_table_update(table_update)?),
"custom_world_agent_message" => db_update.custom_world_agent_message.append(custom_world_agent_message_table::parse_table_update(table_update)?),
"custom_world_agent_operation" => db_update.custom_world_agent_operation.append(custom_world_agent_operation_table::parse_table_update(table_update)?),
"custom_world_agent_session" => db_update.custom_world_agent_session.append(custom_world_agent_session_table::parse_table_update(table_update)?),
"custom_world_draft_card" => db_update.custom_world_draft_card.append(custom_world_draft_card_table::parse_table_update(table_update)?),
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(custom_world_gallery_entry_table::parse_table_update(table_update)?),
"custom_world_profile" => db_update.custom_world_profile.append(custom_world_profile_table::parse_table_update(table_update)?),
"custom_world_session" => db_update.custom_world_session.append(custom_world_session_table::parse_table_update(table_update)?),
"inventory_slot" => db_update.inventory_slot.append(inventory_slot_table::parse_table_update(table_update)?),
"npc_state" => db_update.npc_state.append(npc_state_table::parse_table_update(table_update)?),
"player_progression" => db_update.player_progression.append(player_progression_table::parse_table_update(table_update)?),
"profile_dashboard_state" => db_update.profile_dashboard_state.append(profile_dashboard_state_table::parse_table_update(table_update)?),
"profile_invite_code" => db_update.profile_invite_code.append(profile_invite_code_table::parse_table_update(table_update)?),
"profile_membership" => db_update.profile_membership.append(profile_membership_table::parse_table_update(table_update)?),
"profile_played_world" => db_update.profile_played_world.append(profile_played_world_table::parse_table_update(table_update)?),
"profile_recharge_order" => db_update.profile_recharge_order.append(profile_recharge_order_table::parse_table_update(table_update)?),
"profile_referral_relation" => db_update.profile_referral_relation.append(profile_referral_relation_table::parse_table_update(table_update)?),
"profile_save_archive" => db_update.profile_save_archive.append(profile_save_archive_table::parse_table_update(table_update)?),
"profile_wallet_ledger" => db_update.profile_wallet_ledger.append(profile_wallet_ledger_table::parse_table_update(table_update)?),
"puzzle_agent_message" => db_update.puzzle_agent_message.append(puzzle_agent_message_table::parse_table_update(table_update)?),
"puzzle_agent_session" => db_update.puzzle_agent_session.append(puzzle_agent_session_table::parse_table_update(table_update)?),
"puzzle_runtime_run" => db_update.puzzle_runtime_run.append(puzzle_runtime_run_table::parse_table_update(table_update)?),
"puzzle_work_profile" => db_update.puzzle_work_profile.append(puzzle_work_profile_table::parse_table_update(table_update)?),
"quest_log" => db_update.quest_log.append(quest_log_table::parse_table_update(table_update)?),
"quest_record" => db_update.quest_record.append(quest_record_table::parse_table_update(table_update)?),
"refresh_session" => db_update.refresh_session.append(refresh_session_table::parse_table_update(table_update)?),
"runtime_setting" => db_update.runtime_setting.append(runtime_setting_table::parse_table_update(table_update)?),
"runtime_snapshot" => db_update.runtime_snapshot.append(runtime_snapshot_table::parse_table_update(table_update)?),
"story_event" => db_update.story_event.append(story_event_table::parse_table_update(table_update)?),
"story_session" => db_update.story_session.append(story_session_table::parse_table_update(table_update)?),
"treasure_record" => db_update.treasure_record.append(treasure_record_table::parse_table_update(table_update)?),
"user_account" => db_update.user_account.append(user_account_table::parse_table_update(table_update)?),
"user_browse_history" => db_update.user_browse_history.append(user_browse_history_table::parse_table_update(table_update)?),
unknown => {
return Err(__sdk::InternalError::unknown_name(
@@ -1188,7 +1388,52 @@ impl __sdk::DbUpdate for DbUpdate {
fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache<RemoteModule>) -> AppliedDiff<'_> {
let mut diff = AppliedDiff::default();
diff.custom_world_gallery_entry = cache.apply_diff_to_table::<CustomWorldGalleryEntry>("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id);
diff.ai_result_reference = cache.apply_diff_to_table::<AiResultReference>("ai_result_reference", &self.ai_result_reference).with_updates_by_pk(|row| &row.result_reference_row_id);
diff.ai_task = cache.apply_diff_to_table::<AiTask>("ai_task", &self.ai_task).with_updates_by_pk(|row| &row.task_id);
diff.ai_task_stage = cache.apply_diff_to_table::<AiTaskStage>("ai_task_stage", &self.ai_task_stage).with_updates_by_pk(|row| &row.task_stage_id);
diff.ai_text_chunk = cache.apply_diff_to_table::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk).with_updates_by_pk(|row| &row.text_chunk_row_id);
diff.asset_entity_binding = cache.apply_diff_to_table::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding).with_updates_by_pk(|row| &row.binding_id);
diff.asset_object = cache.apply_diff_to_table::<AssetObject>("asset_object", &self.asset_object).with_updates_by_pk(|row| &row.asset_object_id);
diff.auth_identity = cache.apply_diff_to_table::<AuthIdentity>("auth_identity", &self.auth_identity).with_updates_by_pk(|row| &row.identity_id);
diff.auth_store_snapshot = cache.apply_diff_to_table::<AuthStoreSnapshot>("auth_store_snapshot", &self.auth_store_snapshot).with_updates_by_pk(|row| &row.snapshot_id);
diff.battle_state = cache.apply_diff_to_table::<BattleState>("battle_state", &self.battle_state).with_updates_by_pk(|row| &row.battle_state_id);
diff.big_fish_agent_message = cache.apply_diff_to_table::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message).with_updates_by_pk(|row| &row.message_id);
diff.big_fish_asset_slot = cache.apply_diff_to_table::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot).with_updates_by_pk(|row| &row.slot_id);
diff.big_fish_creation_session = cache.apply_diff_to_table::<BigFishCreationSession>("big_fish_creation_session", &self.big_fish_creation_session).with_updates_by_pk(|row| &row.session_id);
diff.big_fish_runtime_run = cache.apply_diff_to_table::<BigFishRuntimeRun>("big_fish_runtime_run", &self.big_fish_runtime_run).with_updates_by_pk(|row| &row.run_id);
diff.chapter_progression = cache.apply_diff_to_table::<ChapterProgression>("chapter_progression", &self.chapter_progression).with_updates_by_pk(|row| &row.chapter_progression_id);
diff.custom_world_agent_message = cache.apply_diff_to_table::<CustomWorldAgentMessage>("custom_world_agent_message", &self.custom_world_agent_message).with_updates_by_pk(|row| &row.message_id);
diff.custom_world_agent_operation = cache.apply_diff_to_table::<CustomWorldAgentOperation>("custom_world_agent_operation", &self.custom_world_agent_operation).with_updates_by_pk(|row| &row.operation_id);
diff.custom_world_agent_session = cache.apply_diff_to_table::<CustomWorldAgentSession>("custom_world_agent_session", &self.custom_world_agent_session).with_updates_by_pk(|row| &row.session_id);
diff.custom_world_draft_card = cache.apply_diff_to_table::<CustomWorldDraftCard>("custom_world_draft_card", &self.custom_world_draft_card).with_updates_by_pk(|row| &row.card_id);
diff.custom_world_gallery_entry = cache.apply_diff_to_table::<CustomWorldGalleryEntry>("custom_world_gallery_entry", &self.custom_world_gallery_entry).with_updates_by_pk(|row| &row.profile_id);
diff.custom_world_profile = cache.apply_diff_to_table::<CustomWorldProfile>("custom_world_profile", &self.custom_world_profile).with_updates_by_pk(|row| &row.profile_id);
diff.custom_world_session = cache.apply_diff_to_table::<CustomWorldSession>("custom_world_session", &self.custom_world_session).with_updates_by_pk(|row| &row.session_id);
diff.inventory_slot = cache.apply_diff_to_table::<InventorySlot>("inventory_slot", &self.inventory_slot).with_updates_by_pk(|row| &row.slot_id);
diff.npc_state = cache.apply_diff_to_table::<NpcState>("npc_state", &self.npc_state).with_updates_by_pk(|row| &row.npc_state_id);
diff.player_progression = cache.apply_diff_to_table::<PlayerProgression>("player_progression", &self.player_progression).with_updates_by_pk(|row| &row.user_id);
diff.profile_dashboard_state = cache.apply_diff_to_table::<ProfileDashboardState>("profile_dashboard_state", &self.profile_dashboard_state).with_updates_by_pk(|row| &row.user_id);
diff.profile_invite_code = cache.apply_diff_to_table::<ProfileInviteCode>("profile_invite_code", &self.profile_invite_code).with_updates_by_pk(|row| &row.user_id);
diff.profile_membership = cache.apply_diff_to_table::<ProfileMembership>("profile_membership", &self.profile_membership).with_updates_by_pk(|row| &row.user_id);
diff.profile_played_world = cache.apply_diff_to_table::<ProfilePlayedWorld>("profile_played_world", &self.profile_played_world).with_updates_by_pk(|row| &row.played_world_id);
diff.profile_recharge_order = cache.apply_diff_to_table::<ProfileRechargeOrder>("profile_recharge_order", &self.profile_recharge_order).with_updates_by_pk(|row| &row.order_id);
diff.profile_referral_relation = cache.apply_diff_to_table::<ProfileReferralRelation>("profile_referral_relation", &self.profile_referral_relation).with_updates_by_pk(|row| &row.invitee_user_id);
diff.profile_save_archive = cache.apply_diff_to_table::<ProfileSaveArchive>("profile_save_archive", &self.profile_save_archive).with_updates_by_pk(|row| &row.archive_id);
diff.profile_wallet_ledger = cache.apply_diff_to_table::<ProfileWalletLedger>("profile_wallet_ledger", &self.profile_wallet_ledger).with_updates_by_pk(|row| &row.wallet_ledger_id);
diff.puzzle_agent_message = cache.apply_diff_to_table::<PuzzleAgentMessageRow>("puzzle_agent_message", &self.puzzle_agent_message).with_updates_by_pk(|row| &row.message_id);
diff.puzzle_agent_session = cache.apply_diff_to_table::<PuzzleAgentSessionRow>("puzzle_agent_session", &self.puzzle_agent_session).with_updates_by_pk(|row| &row.session_id);
diff.puzzle_runtime_run = cache.apply_diff_to_table::<PuzzleRuntimeRunRow>("puzzle_runtime_run", &self.puzzle_runtime_run).with_updates_by_pk(|row| &row.run_id);
diff.puzzle_work_profile = cache.apply_diff_to_table::<PuzzleWorkProfileRow>("puzzle_work_profile", &self.puzzle_work_profile).with_updates_by_pk(|row| &row.profile_id);
diff.quest_log = cache.apply_diff_to_table::<QuestLog>("quest_log", &self.quest_log).with_updates_by_pk(|row| &row.log_id);
diff.quest_record = cache.apply_diff_to_table::<QuestRecord>("quest_record", &self.quest_record).with_updates_by_pk(|row| &row.quest_id);
diff.refresh_session = cache.apply_diff_to_table::<RefreshSession>("refresh_session", &self.refresh_session).with_updates_by_pk(|row| &row.session_id);
diff.runtime_setting = cache.apply_diff_to_table::<RuntimeSetting>("runtime_setting", &self.runtime_setting).with_updates_by_pk(|row| &row.user_id);
diff.runtime_snapshot = cache.apply_diff_to_table::<RuntimeSnapshotRow>("runtime_snapshot", &self.runtime_snapshot).with_updates_by_pk(|row| &row.user_id);
diff.story_event = cache.apply_diff_to_table::<StoryEvent>("story_event", &self.story_event).with_updates_by_pk(|row| &row.event_id);
diff.story_session = cache.apply_diff_to_table::<StorySession>("story_session", &self.story_session).with_updates_by_pk(|row| &row.story_session_id);
diff.treasure_record = cache.apply_diff_to_table::<TreasureRecord>("treasure_record", &self.treasure_record).with_updates_by_pk(|row| &row.treasure_record_id);
diff.user_account = cache.apply_diff_to_table::<UserAccount>("user_account", &self.user_account).with_updates_by_pk(|row| &row.user_id);
diff.user_browse_history = cache.apply_diff_to_table::<UserBrowseHistory>("user_browse_history", &self.user_browse_history).with_updates_by_pk(|row| &row.browse_history_id);
diff
}
@@ -1196,7 +1441,52 @@ fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {
let mut db_update = DbUpdate::default();
for table_rows in raw.tables {
match &table_rows.table[..] {
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"auth_identity" => db_update.auth_identity.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_invite_code" => db_update.profile_invite_code.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_membership" => db_update.profile_membership.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_recharge_order" => db_update.profile_recharge_order.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_referral_relation" => db_update.profile_referral_relation.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"refresh_session" => db_update.refresh_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"story_event" => db_update.story_event.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"story_session" => db_update.story_session.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"user_account" => db_update.user_account.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); }
}} Ok(db_update)
}
@@ -1204,7 +1494,52 @@ fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {
let mut db_update = DbUpdate::default();
for table_rows in raw.tables {
match &table_rows.table[..] {
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"ai_result_reference" => db_update.ai_result_reference.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"ai_task" => db_update.ai_task.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"ai_task_stage" => db_update.ai_task_stage.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"ai_text_chunk" => db_update.ai_text_chunk.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"asset_entity_binding" => db_update.asset_entity_binding.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"asset_object" => db_update.asset_object.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"auth_identity" => db_update.auth_identity.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"auth_store_snapshot" => db_update.auth_store_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"battle_state" => db_update.battle_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"big_fish_agent_message" => db_update.big_fish_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"big_fish_asset_slot" => db_update.big_fish_asset_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"big_fish_creation_session" => db_update.big_fish_creation_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"big_fish_runtime_run" => db_update.big_fish_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"chapter_progression" => db_update.chapter_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_agent_message" => db_update.custom_world_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_agent_operation" => db_update.custom_world_agent_operation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_agent_session" => db_update.custom_world_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_draft_card" => db_update.custom_world_draft_card.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_gallery_entry" => db_update.custom_world_gallery_entry.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_profile" => db_update.custom_world_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"custom_world_session" => db_update.custom_world_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"inventory_slot" => db_update.inventory_slot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"npc_state" => db_update.npc_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"player_progression" => db_update.player_progression.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_dashboard_state" => db_update.profile_dashboard_state.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_invite_code" => db_update.profile_invite_code.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_membership" => db_update.profile_membership.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_played_world" => db_update.profile_played_world.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_recharge_order" => db_update.profile_recharge_order.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_referral_relation" => db_update.profile_referral_relation.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_save_archive" => db_update.profile_save_archive.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"profile_wallet_ledger" => db_update.profile_wallet_ledger.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"puzzle_agent_message" => db_update.puzzle_agent_message.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"puzzle_agent_session" => db_update.puzzle_agent_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"puzzle_runtime_run" => db_update.puzzle_runtime_run.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"puzzle_work_profile" => db_update.puzzle_work_profile.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"quest_log" => db_update.quest_log.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"quest_record" => db_update.quest_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"refresh_session" => db_update.refresh_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"runtime_setting" => db_update.runtime_setting.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"runtime_snapshot" => db_update.runtime_snapshot.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"story_event" => db_update.story_event.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"story_session" => db_update.story_session.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"treasure_record" => db_update.treasure_record.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"user_account" => db_update.user_account.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"user_browse_history" => db_update.user_browse_history.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
unknown => { return Err(__sdk::InternalError::unknown_name("table", unknown, "QueryRows").into()); }
}} Ok(db_update)
}
@@ -1214,7 +1549,52 @@ for table_rows in raw.tables {
#[allow(non_snake_case)]
#[doc(hidden)]
pub struct AppliedDiff<'r> {
custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>,
ai_result_reference: __sdk::TableAppliedDiff<'r, AiResultReference>,
ai_task: __sdk::TableAppliedDiff<'r, AiTask>,
ai_task_stage: __sdk::TableAppliedDiff<'r, AiTaskStage>,
ai_text_chunk: __sdk::TableAppliedDiff<'r, AiTextChunk>,
asset_entity_binding: __sdk::TableAppliedDiff<'r, AssetEntityBinding>,
asset_object: __sdk::TableAppliedDiff<'r, AssetObject>,
auth_identity: __sdk::TableAppliedDiff<'r, AuthIdentity>,
auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>,
battle_state: __sdk::TableAppliedDiff<'r, BattleState>,
big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>,
big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>,
big_fish_creation_session: __sdk::TableAppliedDiff<'r, BigFishCreationSession>,
big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>,
chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>,
custom_world_agent_message: __sdk::TableAppliedDiff<'r, CustomWorldAgentMessage>,
custom_world_agent_operation: __sdk::TableAppliedDiff<'r, CustomWorldAgentOperation>,
custom_world_agent_session: __sdk::TableAppliedDiff<'r, CustomWorldAgentSession>,
custom_world_draft_card: __sdk::TableAppliedDiff<'r, CustomWorldDraftCard>,
custom_world_gallery_entry: __sdk::TableAppliedDiff<'r, CustomWorldGalleryEntry>,
custom_world_profile: __sdk::TableAppliedDiff<'r, CustomWorldProfile>,
custom_world_session: __sdk::TableAppliedDiff<'r, CustomWorldSession>,
inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>,
npc_state: __sdk::TableAppliedDiff<'r, NpcState>,
player_progression: __sdk::TableAppliedDiff<'r, PlayerProgression>,
profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>,
profile_invite_code: __sdk::TableAppliedDiff<'r, ProfileInviteCode>,
profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>,
profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>,
profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>,
profile_referral_relation: __sdk::TableAppliedDiff<'r, ProfileReferralRelation>,
profile_save_archive: __sdk::TableAppliedDiff<'r, ProfileSaveArchive>,
profile_wallet_ledger: __sdk::TableAppliedDiff<'r, ProfileWalletLedger>,
puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>,
puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>,
puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>,
puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>,
quest_log: __sdk::TableAppliedDiff<'r, QuestLog>,
quest_record: __sdk::TableAppliedDiff<'r, QuestRecord>,
refresh_session: __sdk::TableAppliedDiff<'r, RefreshSession>,
runtime_setting: __sdk::TableAppliedDiff<'r, RuntimeSetting>,
runtime_snapshot: __sdk::TableAppliedDiff<'r, RuntimeSnapshotRow>,
story_event: __sdk::TableAppliedDiff<'r, StoryEvent>,
story_session: __sdk::TableAppliedDiff<'r, StorySession>,
treasure_record: __sdk::TableAppliedDiff<'r, TreasureRecord>,
user_account: __sdk::TableAppliedDiff<'r, UserAccount>,
user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>,
__unused: std::marker::PhantomData<&'r ()>,
}
@@ -1225,7 +1605,52 @@ impl __sdk::InModule for AppliedDiff<'_> {
impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks<RemoteModule>) {
callbacks.invoke_table_row_callbacks::<CustomWorldGalleryEntry>("custom_world_gallery_entry", &self.custom_world_gallery_entry, event);
callbacks.invoke_table_row_callbacks::<AiResultReference>("ai_result_reference", &self.ai_result_reference, event);
callbacks.invoke_table_row_callbacks::<AiTask>("ai_task", &self.ai_task, event);
callbacks.invoke_table_row_callbacks::<AiTaskStage>("ai_task_stage", &self.ai_task_stage, event);
callbacks.invoke_table_row_callbacks::<AiTextChunk>("ai_text_chunk", &self.ai_text_chunk, event);
callbacks.invoke_table_row_callbacks::<AssetEntityBinding>("asset_entity_binding", &self.asset_entity_binding, event);
callbacks.invoke_table_row_callbacks::<AssetObject>("asset_object", &self.asset_object, event);
callbacks.invoke_table_row_callbacks::<AuthIdentity>("auth_identity", &self.auth_identity, event);
callbacks.invoke_table_row_callbacks::<AuthStoreSnapshot>("auth_store_snapshot", &self.auth_store_snapshot, event);
callbacks.invoke_table_row_callbacks::<BattleState>("battle_state", &self.battle_state, event);
callbacks.invoke_table_row_callbacks::<BigFishAgentMessage>("big_fish_agent_message", &self.big_fish_agent_message, event);
callbacks.invoke_table_row_callbacks::<BigFishAssetSlot>("big_fish_asset_slot", &self.big_fish_asset_slot, event);
callbacks.invoke_table_row_callbacks::<BigFishCreationSession>("big_fish_creation_session", &self.big_fish_creation_session, event);
callbacks.invoke_table_row_callbacks::<BigFishRuntimeRun>("big_fish_runtime_run", &self.big_fish_runtime_run, event);
callbacks.invoke_table_row_callbacks::<ChapterProgression>("chapter_progression", &self.chapter_progression, event);
callbacks.invoke_table_row_callbacks::<CustomWorldAgentMessage>("custom_world_agent_message", &self.custom_world_agent_message, event);
callbacks.invoke_table_row_callbacks::<CustomWorldAgentOperation>("custom_world_agent_operation", &self.custom_world_agent_operation, event);
callbacks.invoke_table_row_callbacks::<CustomWorldAgentSession>("custom_world_agent_session", &self.custom_world_agent_session, event);
callbacks.invoke_table_row_callbacks::<CustomWorldDraftCard>("custom_world_draft_card", &self.custom_world_draft_card, event);
callbacks.invoke_table_row_callbacks::<CustomWorldGalleryEntry>("custom_world_gallery_entry", &self.custom_world_gallery_entry, event);
callbacks.invoke_table_row_callbacks::<CustomWorldProfile>("custom_world_profile", &self.custom_world_profile, event);
callbacks.invoke_table_row_callbacks::<CustomWorldSession>("custom_world_session", &self.custom_world_session, event);
callbacks.invoke_table_row_callbacks::<InventorySlot>("inventory_slot", &self.inventory_slot, event);
callbacks.invoke_table_row_callbacks::<NpcState>("npc_state", &self.npc_state, event);
callbacks.invoke_table_row_callbacks::<PlayerProgression>("player_progression", &self.player_progression, event);
callbacks.invoke_table_row_callbacks::<ProfileDashboardState>("profile_dashboard_state", &self.profile_dashboard_state, event);
callbacks.invoke_table_row_callbacks::<ProfileInviteCode>("profile_invite_code", &self.profile_invite_code, event);
callbacks.invoke_table_row_callbacks::<ProfileMembership>("profile_membership", &self.profile_membership, event);
callbacks.invoke_table_row_callbacks::<ProfilePlayedWorld>("profile_played_world", &self.profile_played_world, event);
callbacks.invoke_table_row_callbacks::<ProfileRechargeOrder>("profile_recharge_order", &self.profile_recharge_order, event);
callbacks.invoke_table_row_callbacks::<ProfileReferralRelation>("profile_referral_relation", &self.profile_referral_relation, event);
callbacks.invoke_table_row_callbacks::<ProfileSaveArchive>("profile_save_archive", &self.profile_save_archive, event);
callbacks.invoke_table_row_callbacks::<ProfileWalletLedger>("profile_wallet_ledger", &self.profile_wallet_ledger, event);
callbacks.invoke_table_row_callbacks::<PuzzleAgentMessageRow>("puzzle_agent_message", &self.puzzle_agent_message, event);
callbacks.invoke_table_row_callbacks::<PuzzleAgentSessionRow>("puzzle_agent_session", &self.puzzle_agent_session, event);
callbacks.invoke_table_row_callbacks::<PuzzleRuntimeRunRow>("puzzle_runtime_run", &self.puzzle_runtime_run, event);
callbacks.invoke_table_row_callbacks::<PuzzleWorkProfileRow>("puzzle_work_profile", &self.puzzle_work_profile, event);
callbacks.invoke_table_row_callbacks::<QuestLog>("quest_log", &self.quest_log, event);
callbacks.invoke_table_row_callbacks::<QuestRecord>("quest_record", &self.quest_record, event);
callbacks.invoke_table_row_callbacks::<RefreshSession>("refresh_session", &self.refresh_session, event);
callbacks.invoke_table_row_callbacks::<RuntimeSetting>("runtime_setting", &self.runtime_setting, event);
callbacks.invoke_table_row_callbacks::<RuntimeSnapshotRow>("runtime_snapshot", &self.runtime_snapshot, event);
callbacks.invoke_table_row_callbacks::<StoryEvent>("story_event", &self.story_event, event);
callbacks.invoke_table_row_callbacks::<StorySession>("story_session", &self.story_session, event);
callbacks.invoke_table_row_callbacks::<TreasureRecord>("treasure_record", &self.treasure_record, event);
callbacks.invoke_table_row_callbacks::<UserAccount>("user_account", &self.user_account, event);
callbacks.invoke_table_row_callbacks::<UserBrowseHistory>("user_browse_history", &self.user_browse_history, event);
}
}
@@ -1877,9 +2302,99 @@ impl __sdk::SpacetimeModule for RemoteModule {
type QueryBuilder = __sdk::QueryBuilder;
fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {
custom_world_gallery_entry_table::register_table(client_cache);
ai_result_reference_table::register_table(client_cache);
ai_task_table::register_table(client_cache);
ai_task_stage_table::register_table(client_cache);
ai_text_chunk_table::register_table(client_cache);
asset_entity_binding_table::register_table(client_cache);
asset_object_table::register_table(client_cache);
auth_identity_table::register_table(client_cache);
auth_store_snapshot_table::register_table(client_cache);
battle_state_table::register_table(client_cache);
big_fish_agent_message_table::register_table(client_cache);
big_fish_asset_slot_table::register_table(client_cache);
big_fish_creation_session_table::register_table(client_cache);
big_fish_runtime_run_table::register_table(client_cache);
chapter_progression_table::register_table(client_cache);
custom_world_agent_message_table::register_table(client_cache);
custom_world_agent_operation_table::register_table(client_cache);
custom_world_agent_session_table::register_table(client_cache);
custom_world_draft_card_table::register_table(client_cache);
custom_world_gallery_entry_table::register_table(client_cache);
custom_world_profile_table::register_table(client_cache);
custom_world_session_table::register_table(client_cache);
inventory_slot_table::register_table(client_cache);
npc_state_table::register_table(client_cache);
player_progression_table::register_table(client_cache);
profile_dashboard_state_table::register_table(client_cache);
profile_invite_code_table::register_table(client_cache);
profile_membership_table::register_table(client_cache);
profile_played_world_table::register_table(client_cache);
profile_recharge_order_table::register_table(client_cache);
profile_referral_relation_table::register_table(client_cache);
profile_save_archive_table::register_table(client_cache);
profile_wallet_ledger_table::register_table(client_cache);
puzzle_agent_message_table::register_table(client_cache);
puzzle_agent_session_table::register_table(client_cache);
puzzle_runtime_run_table::register_table(client_cache);
puzzle_work_profile_table::register_table(client_cache);
quest_log_table::register_table(client_cache);
quest_record_table::register_table(client_cache);
refresh_session_table::register_table(client_cache);
runtime_setting_table::register_table(client_cache);
runtime_snapshot_table::register_table(client_cache);
story_event_table::register_table(client_cache);
story_session_table::register_table(client_cache);
treasure_record_table::register_table(client_cache);
user_account_table::register_table(client_cache);
user_browse_history_table::register_table(client_cache);
}
const ALL_TABLE_NAMES: &'static [&'static str] = &[
"custom_world_gallery_entry",
"ai_result_reference",
"ai_task",
"ai_task_stage",
"ai_text_chunk",
"asset_entity_binding",
"asset_object",
"auth_identity",
"auth_store_snapshot",
"battle_state",
"big_fish_agent_message",
"big_fish_asset_slot",
"big_fish_creation_session",
"big_fish_runtime_run",
"chapter_progression",
"custom_world_agent_message",
"custom_world_agent_operation",
"custom_world_agent_session",
"custom_world_draft_card",
"custom_world_gallery_entry",
"custom_world_profile",
"custom_world_session",
"inventory_slot",
"npc_state",
"player_progression",
"profile_dashboard_state",
"profile_invite_code",
"profile_membership",
"profile_played_world",
"profile_recharge_order",
"profile_referral_relation",
"profile_save_archive",
"profile_wallet_ledger",
"puzzle_agent_message",
"puzzle_agent_session",
"puzzle_runtime_run",
"puzzle_work_profile",
"quest_log",
"quest_record",
"refresh_session",
"runtime_setting",
"runtime_snapshot",
"story_event",
"story_session",
"treasure_record",
"user_account",
"user_browse_history",
];
}

View File

@@ -0,0 +1,194 @@
// 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::profile_invite_code_type::ProfileInviteCode;
/// Table handle for the table `profile_invite_code`.
///
/// Obtain a handle from the [`ProfileInviteCodeTableAccess::profile_invite_code`] method on [`super::RemoteTables`],
/// like `ctx.db.profile_invite_code()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_invite_code().on_insert(...)`.
pub struct ProfileInviteCodeTableHandle<'ctx> {
imp: __sdk::TableHandle<ProfileInviteCode>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `profile_invite_code`.
///
/// Implemented for [`super::RemoteTables`].
pub trait ProfileInviteCodeTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`ProfileInviteCodeTableHandle`], which mediates access to the table `profile_invite_code`.
fn profile_invite_code(&self) -> ProfileInviteCodeTableHandle<'_>;
}
impl ProfileInviteCodeTableAccess for super::RemoteTables {
fn profile_invite_code(&self) -> ProfileInviteCodeTableHandle<'_> {
ProfileInviteCodeTableHandle {
imp: self.imp.get_table::<ProfileInviteCode>("profile_invite_code"),
ctx: std::marker::PhantomData,
}
}
}
pub struct ProfileInviteCodeInsertCallbackId(__sdk::CallbackId);
pub struct ProfileInviteCodeDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for ProfileInviteCodeTableHandle<'ctx> {
type Row = ProfileInviteCode;
type EventContext = super::EventContext;
fn count(&self) -> u64 { self.imp.count() }
fn iter(&self) -> impl Iterator<Item = ProfileInviteCode> + '_ { self.imp.iter() }
type InsertCallbackId = ProfileInviteCodeInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileInviteCodeInsertCallbackId {
ProfileInviteCodeInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: ProfileInviteCodeInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = ProfileInviteCodeDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileInviteCodeDeleteCallbackId {
ProfileInviteCodeDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: ProfileInviteCodeDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct ProfileInviteCodeUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileInviteCodeTableHandle<'ctx> {
type UpdateCallbackId = ProfileInviteCodeUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> ProfileInviteCodeUpdateCallbackId {
ProfileInviteCodeUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: ProfileInviteCodeUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `user_id` unique index on the table `profile_invite_code`,
/// which allows point queries on the field of the same name
/// via the [`ProfileInviteCodeUserIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_invite_code().user_id().find(...)`.
pub struct ProfileInviteCodeUserIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileInviteCode, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileInviteCodeTableHandle<'ctx> {
/// Get a handle on the `user_id` unique index on the table `profile_invite_code`.
pub fn user_id(&self) -> ProfileInviteCodeUserIdUnique<'ctx> {
ProfileInviteCodeUserIdUnique {
imp: self.imp.get_unique_constraint::<String>("user_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileInviteCodeUserIdUnique<'ctx> {
/// Find the subscribed row whose `user_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileInviteCode> {
self.imp.find(col_val)
}
}
/// Access to the `invite_code` unique index on the table `profile_invite_code`,
/// which allows point queries on the field of the same name
/// via the [`ProfileInviteCodeInviteCodeUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_invite_code().invite_code().find(...)`.
pub struct ProfileInviteCodeInviteCodeUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileInviteCode, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileInviteCodeTableHandle<'ctx> {
/// Get a handle on the `invite_code` unique index on the table `profile_invite_code`.
pub fn invite_code(&self) -> ProfileInviteCodeInviteCodeUnique<'ctx> {
ProfileInviteCodeInviteCodeUnique {
imp: self.imp.get_unique_constraint::<String>("invite_code"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileInviteCodeInviteCodeUnique<'ctx> {
/// Find the subscribed row whose `invite_code` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileInviteCode> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<ProfileInviteCode>("profile_invite_code");
_table.add_unique_constraint::<String>("user_id", |row| &row.user_id);
_table.add_unique_constraint::<String>("invite_code", |row| &row.invite_code);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileInviteCode>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<ProfileInviteCode>",
"TableUpdate",
).with_cause(e).into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `ProfileInviteCode`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait profile_invite_codeQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `ProfileInviteCode`.
fn profile_invite_code(&self) -> __sdk::__query_builder::Table<ProfileInviteCode>;
}
impl profile_invite_codeQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_invite_code(&self) -> __sdk::__query_builder::Table<ProfileInviteCode> {
__sdk::__query_builder::Table::new("profile_invite_code")
}
}

View File

@@ -0,0 +1,71 @@
// 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 ProfileInviteCode {
pub user_id: String,
pub invite_code: String,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for ProfileInviteCode {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `ProfileInviteCode`.
///
/// Provides typed access to columns for query building.
pub struct ProfileInviteCodeCols {
pub user_id: __sdk::__query_builder::Col<ProfileInviteCode, String>,
pub invite_code: __sdk::__query_builder::Col<ProfileInviteCode, String>,
pub created_at: __sdk::__query_builder::Col<ProfileInviteCode, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<ProfileInviteCode, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for ProfileInviteCode {
type Cols = ProfileInviteCodeCols;
fn cols(table_name: &'static str) -> Self::Cols {
ProfileInviteCodeCols {
user_id: __sdk::__query_builder::Col::new(table_name, "user_id"),
invite_code: __sdk::__query_builder::Col::new(table_name, "invite_code"),
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 `ProfileInviteCode`.
///
/// Provides typed access to indexed columns for query building.
pub struct ProfileInviteCodeIxCols {
pub invite_code: __sdk::__query_builder::IxCol<ProfileInviteCode, String>,
pub user_id: __sdk::__query_builder::IxCol<ProfileInviteCode, String>,
}
impl __sdk::__query_builder::HasIxCols for ProfileInviteCode {
type IxCols = ProfileInviteCodeIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
ProfileInviteCodeIxCols {
invite_code: __sdk::__query_builder::IxCol::new(table_name, "invite_code"),
user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for ProfileInviteCode {}

View File

@@ -0,0 +1,165 @@
// 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::profile_membership_type::ProfileMembership;
use super::runtime_profile_membership_status_type::RuntimeProfileMembershipStatus;
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
/// Table handle for the table `profile_membership`.
///
/// Obtain a handle from the [`ProfileMembershipTableAccess::profile_membership`] method on [`super::RemoteTables`],
/// like `ctx.db.profile_membership()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_membership().on_insert(...)`.
pub struct ProfileMembershipTableHandle<'ctx> {
imp: __sdk::TableHandle<ProfileMembership>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `profile_membership`.
///
/// Implemented for [`super::RemoteTables`].
pub trait ProfileMembershipTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`ProfileMembershipTableHandle`], which mediates access to the table `profile_membership`.
fn profile_membership(&self) -> ProfileMembershipTableHandle<'_>;
}
impl ProfileMembershipTableAccess for super::RemoteTables {
fn profile_membership(&self) -> ProfileMembershipTableHandle<'_> {
ProfileMembershipTableHandle {
imp: self.imp.get_table::<ProfileMembership>("profile_membership"),
ctx: std::marker::PhantomData,
}
}
}
pub struct ProfileMembershipInsertCallbackId(__sdk::CallbackId);
pub struct ProfileMembershipDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for ProfileMembershipTableHandle<'ctx> {
type Row = ProfileMembership;
type EventContext = super::EventContext;
fn count(&self) -> u64 { self.imp.count() }
fn iter(&self) -> impl Iterator<Item = ProfileMembership> + '_ { self.imp.iter() }
type InsertCallbackId = ProfileMembershipInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileMembershipInsertCallbackId {
ProfileMembershipInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: ProfileMembershipInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = ProfileMembershipDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileMembershipDeleteCallbackId {
ProfileMembershipDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: ProfileMembershipDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct ProfileMembershipUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileMembershipTableHandle<'ctx> {
type UpdateCallbackId = ProfileMembershipUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> ProfileMembershipUpdateCallbackId {
ProfileMembershipUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: ProfileMembershipUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `user_id` unique index on the table `profile_membership`,
/// which allows point queries on the field of the same name
/// via the [`ProfileMembershipUserIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_membership().user_id().find(...)`.
pub struct ProfileMembershipUserIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileMembership, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileMembershipTableHandle<'ctx> {
/// Get a handle on the `user_id` unique index on the table `profile_membership`.
pub fn user_id(&self) -> ProfileMembershipUserIdUnique<'ctx> {
ProfileMembershipUserIdUnique {
imp: self.imp.get_unique_constraint::<String>("user_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileMembershipUserIdUnique<'ctx> {
/// Find the subscribed row whose `user_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileMembership> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<ProfileMembership>("profile_membership");
_table.add_unique_constraint::<String>("user_id", |row| &row.user_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileMembership>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<ProfileMembership>",
"TableUpdate",
).with_cause(e).into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `ProfileMembership`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait profile_membershipQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `ProfileMembership`.
fn profile_membership(&self) -> __sdk::__query_builder::Table<ProfileMembership>;
}
impl profile_membershipQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_membership(&self) -> __sdk::__query_builder::Table<ProfileMembership> {
__sdk::__query_builder::Table::new("profile_membership")
}
}

View File

@@ -0,0 +1,165 @@
// 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::profile_recharge_order_type::ProfileRechargeOrder;
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
use super::runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
/// Table handle for the table `profile_recharge_order`.
///
/// Obtain a handle from the [`ProfileRechargeOrderTableAccess::profile_recharge_order`] method on [`super::RemoteTables`],
/// like `ctx.db.profile_recharge_order()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_recharge_order().on_insert(...)`.
pub struct ProfileRechargeOrderTableHandle<'ctx> {
imp: __sdk::TableHandle<ProfileRechargeOrder>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `profile_recharge_order`.
///
/// Implemented for [`super::RemoteTables`].
pub trait ProfileRechargeOrderTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`ProfileRechargeOrderTableHandle`], which mediates access to the table `profile_recharge_order`.
fn profile_recharge_order(&self) -> ProfileRechargeOrderTableHandle<'_>;
}
impl ProfileRechargeOrderTableAccess for super::RemoteTables {
fn profile_recharge_order(&self) -> ProfileRechargeOrderTableHandle<'_> {
ProfileRechargeOrderTableHandle {
imp: self.imp.get_table::<ProfileRechargeOrder>("profile_recharge_order"),
ctx: std::marker::PhantomData,
}
}
}
pub struct ProfileRechargeOrderInsertCallbackId(__sdk::CallbackId);
pub struct ProfileRechargeOrderDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for ProfileRechargeOrderTableHandle<'ctx> {
type Row = ProfileRechargeOrder;
type EventContext = super::EventContext;
fn count(&self) -> u64 { self.imp.count() }
fn iter(&self) -> impl Iterator<Item = ProfileRechargeOrder> + '_ { self.imp.iter() }
type InsertCallbackId = ProfileRechargeOrderInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileRechargeOrderInsertCallbackId {
ProfileRechargeOrderInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: ProfileRechargeOrderInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = ProfileRechargeOrderDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileRechargeOrderDeleteCallbackId {
ProfileRechargeOrderDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: ProfileRechargeOrderDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct ProfileRechargeOrderUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileRechargeOrderTableHandle<'ctx> {
type UpdateCallbackId = ProfileRechargeOrderUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> ProfileRechargeOrderUpdateCallbackId {
ProfileRechargeOrderUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: ProfileRechargeOrderUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `order_id` unique index on the table `profile_recharge_order`,
/// which allows point queries on the field of the same name
/// via the [`ProfileRechargeOrderOrderIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_recharge_order().order_id().find(...)`.
pub struct ProfileRechargeOrderOrderIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileRechargeOrder, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileRechargeOrderTableHandle<'ctx> {
/// Get a handle on the `order_id` unique index on the table `profile_recharge_order`.
pub fn order_id(&self) -> ProfileRechargeOrderOrderIdUnique<'ctx> {
ProfileRechargeOrderOrderIdUnique {
imp: self.imp.get_unique_constraint::<String>("order_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileRechargeOrderOrderIdUnique<'ctx> {
/// Find the subscribed row whose `order_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileRechargeOrder> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<ProfileRechargeOrder>("profile_recharge_order");
_table.add_unique_constraint::<String>("order_id", |row| &row.order_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileRechargeOrder>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<ProfileRechargeOrder>",
"TableUpdate",
).with_cause(e).into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `ProfileRechargeOrder`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait profile_recharge_orderQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `ProfileRechargeOrder`.
fn profile_recharge_order(&self) -> __sdk::__query_builder::Table<ProfileRechargeOrder>;
}
impl profile_recharge_orderQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_recharge_order(&self) -> __sdk::__query_builder::Table<ProfileRechargeOrder> {
__sdk::__query_builder::Table::new("profile_recharge_order")
}
}

View File

@@ -0,0 +1,163 @@
// 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::profile_referral_relation_type::ProfileReferralRelation;
/// Table handle for the table `profile_referral_relation`.
///
/// Obtain a handle from the [`ProfileReferralRelationTableAccess::profile_referral_relation`] method on [`super::RemoteTables`],
/// like `ctx.db.profile_referral_relation()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_referral_relation().on_insert(...)`.
pub struct ProfileReferralRelationTableHandle<'ctx> {
imp: __sdk::TableHandle<ProfileReferralRelation>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `profile_referral_relation`.
///
/// Implemented for [`super::RemoteTables`].
pub trait ProfileReferralRelationTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`ProfileReferralRelationTableHandle`], which mediates access to the table `profile_referral_relation`.
fn profile_referral_relation(&self) -> ProfileReferralRelationTableHandle<'_>;
}
impl ProfileReferralRelationTableAccess for super::RemoteTables {
fn profile_referral_relation(&self) -> ProfileReferralRelationTableHandle<'_> {
ProfileReferralRelationTableHandle {
imp: self.imp.get_table::<ProfileReferralRelation>("profile_referral_relation"),
ctx: std::marker::PhantomData,
}
}
}
pub struct ProfileReferralRelationInsertCallbackId(__sdk::CallbackId);
pub struct ProfileReferralRelationDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for ProfileReferralRelationTableHandle<'ctx> {
type Row = ProfileReferralRelation;
type EventContext = super::EventContext;
fn count(&self) -> u64 { self.imp.count() }
fn iter(&self) -> impl Iterator<Item = ProfileReferralRelation> + '_ { self.imp.iter() }
type InsertCallbackId = ProfileReferralRelationInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileReferralRelationInsertCallbackId {
ProfileReferralRelationInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: ProfileReferralRelationInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = ProfileReferralRelationDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> ProfileReferralRelationDeleteCallbackId {
ProfileReferralRelationDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: ProfileReferralRelationDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct ProfileReferralRelationUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileReferralRelationTableHandle<'ctx> {
type UpdateCallbackId = ProfileReferralRelationUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> ProfileReferralRelationUpdateCallbackId {
ProfileReferralRelationUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: ProfileReferralRelationUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `invitee_user_id` unique index on the table `profile_referral_relation`,
/// which allows point queries on the field of the same name
/// via the [`ProfileReferralRelationInviteeUserIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.profile_referral_relation().invitee_user_id().find(...)`.
pub struct ProfileReferralRelationInviteeUserIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<ProfileReferralRelation, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> ProfileReferralRelationTableHandle<'ctx> {
/// Get a handle on the `invitee_user_id` unique index on the table `profile_referral_relation`.
pub fn invitee_user_id(&self) -> ProfileReferralRelationInviteeUserIdUnique<'ctx> {
ProfileReferralRelationInviteeUserIdUnique {
imp: self.imp.get_unique_constraint::<String>("invitee_user_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> ProfileReferralRelationInviteeUserIdUnique<'ctx> {
/// Find the subscribed row whose `invitee_user_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &String) -> Option<ProfileReferralRelation> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<ProfileReferralRelation>("profile_referral_relation");
_table.add_unique_constraint::<String>("invitee_user_id", |row| &row.invitee_user_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileReferralRelation>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<ProfileReferralRelation>",
"TableUpdate",
).with_cause(e).into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `ProfileReferralRelation`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait profile_referral_relationQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `ProfileReferralRelation`.
fn profile_referral_relation(&self) -> __sdk::__query_builder::Table<ProfileReferralRelation>;
}
impl profile_referral_relationQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_referral_relation(&self) -> __sdk::__query_builder::Table<ProfileReferralRelation> {
__sdk::__query_builder::Table::new("profile_referral_relation")
}
}

View File

@@ -0,0 +1,77 @@
// 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 ProfileReferralRelation {
pub invitee_user_id: String,
pub inviter_user_id: String,
pub invite_code: String,
pub inviter_reward_granted: bool,
pub invitee_reward_granted: bool,
pub bound_at: __sdk::Timestamp,
}
impl __sdk::InModule for ProfileReferralRelation {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `ProfileReferralRelation`.
///
/// Provides typed access to columns for query building.
pub struct ProfileReferralRelationCols {
pub invitee_user_id: __sdk::__query_builder::Col<ProfileReferralRelation, String>,
pub inviter_user_id: __sdk::__query_builder::Col<ProfileReferralRelation, String>,
pub invite_code: __sdk::__query_builder::Col<ProfileReferralRelation, String>,
pub inviter_reward_granted: __sdk::__query_builder::Col<ProfileReferralRelation, bool>,
pub invitee_reward_granted: __sdk::__query_builder::Col<ProfileReferralRelation, bool>,
pub bound_at: __sdk::__query_builder::Col<ProfileReferralRelation, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for ProfileReferralRelation {
type Cols = ProfileReferralRelationCols;
fn cols(table_name: &'static str) -> Self::Cols {
ProfileReferralRelationCols {
invitee_user_id: __sdk::__query_builder::Col::new(table_name, "invitee_user_id"),
inviter_user_id: __sdk::__query_builder::Col::new(table_name, "inviter_user_id"),
invite_code: __sdk::__query_builder::Col::new(table_name, "invite_code"),
inviter_reward_granted: __sdk::__query_builder::Col::new(table_name, "inviter_reward_granted"),
invitee_reward_granted: __sdk::__query_builder::Col::new(table_name, "invitee_reward_granted"),
bound_at: __sdk::__query_builder::Col::new(table_name, "bound_at"),
}
}
}
/// Indexed column accessor struct for the table `ProfileReferralRelation`.
///
/// Provides typed access to indexed columns for query building.
pub struct ProfileReferralRelationIxCols {
pub invitee_user_id: __sdk::__query_builder::IxCol<ProfileReferralRelation, String>,
pub inviter_user_id: __sdk::__query_builder::IxCol<ProfileReferralRelation, String>,
}
impl __sdk::__query_builder::HasIxCols for ProfileReferralRelation {
type IxCols = ProfileReferralRelationIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
ProfileReferralRelationIxCols {
invitee_user_id: __sdk::__query_builder::IxCol::new(table_name, "invitee_user_id"),
inviter_user_id: __sdk::__query_builder::IxCol::new(table_name, "inviter_user_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for ProfileReferralRelation {}

View File

@@ -0,0 +1,58 @@
// 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_referral_redeem_input_type::RuntimeReferralRedeemInput;
use super::runtime_referral_redeem_procedure_result_type::RuntimeReferralRedeemProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct RedeemProfileReferralInviteCodeArgs {
pub input: RuntimeReferralRedeemInput,
}
impl __sdk::InModule for RedeemProfileReferralInviteCodeArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `redeem_profile_referral_invite_code`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait redeem_profile_referral_invite_code {
fn redeem_profile_referral_invite_code(&self, input: RuntimeReferralRedeemInput,
) {
self.redeem_profile_referral_invite_code_then(input, |_, _| {});
}
fn redeem_profile_referral_invite_code_then(
&self,
input: RuntimeReferralRedeemInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeReferralRedeemProcedureResult, __sdk::InternalError>) + Send + 'static,
);
}
impl redeem_profile_referral_invite_code for super::RemoteProcedures {
fn redeem_profile_referral_invite_code_then(
&self,
input: RuntimeReferralRedeemInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<RuntimeReferralRedeemProcedureResult, __sdk::InternalError>) + Send + 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeReferralRedeemProcedureResult>(
"redeem_profile_referral_invite_code",
RedeemProfileReferralInviteCodeArgs { input, },
__callback,
);
}
}

View File

@@ -0,0 +1,23 @@
// 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 RuntimeReferralInviteCenterGetInput {
pub user_id: String,
}
impl __sdk::InModule for RuntimeReferralInviteCenterGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,26 @@
// 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_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeReferralInviteCenterProcedureResult {
pub ok: bool,
pub record: Option::<RuntimeReferralInviteCenterSnapshot>,
pub error_message: Option::<String>,
}
impl __sdk::InModule for RuntimeReferralInviteCenterProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,34 @@
// 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 RuntimeReferralInviteCenterSnapshot {
pub user_id: String,
pub invite_code: String,
pub invite_link_path: String,
pub invited_count: u32,
pub rewarded_invite_count: u32,
pub today_inviter_reward_count: u32,
pub today_inviter_reward_remaining: u32,
pub reward_points: u64,
pub has_redeemed_code: bool,
pub bound_inviter_user_id: Option::<String>,
pub bound_at_micros: Option::<i64>,
pub updated_at_micros: i64,
}
impl __sdk::InModule for RuntimeReferralInviteCenterSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,25 @@
// 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 RuntimeReferralRedeemInput {
pub user_id: String,
pub invite_code: String,
pub updated_at_micros: i64,
}
impl __sdk::InModule for RuntimeReferralRedeemInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,26 @@
// 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_referral_redeem_snapshot_type::RuntimeReferralRedeemSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeReferralRedeemProcedureResult {
pub ok: bool,
pub record: Option::<RuntimeReferralRedeemSnapshot>,
pub error_message: Option::<String>,
}
impl __sdk::InModule for RuntimeReferralRedeemProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,28 @@
// 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_referral_invite_center_snapshot_type::RuntimeReferralInviteCenterSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeReferralRedeemSnapshot {
pub center: RuntimeReferralInviteCenterSnapshot,
pub invitee_reward_granted: bool,
pub inviter_reward_granted: bool,
pub invitee_balance_after: u64,
pub inviter_balance_after: u64,
}
impl __sdk::InModule for RuntimeReferralRedeemSnapshot {
type Module = super::RemoteModule;
}

View File

@@ -149,6 +149,51 @@ impl SpacetimeClient {
.await
}
pub async fn get_profile_referral_invite_center(
&self,
user_id: String,
) -> Result<RuntimeReferralInviteCenterRecord, SpacetimeClientError> {
let procedure_input = build_runtime_referral_invite_center_get_input(user_id)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?
.into();
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.get_profile_referral_invite_center_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_runtime_referral_invite_center_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn redeem_profile_referral_invite_code(
&self,
user_id: String,
invite_code: String,
updated_at_micros: i64,
) -> Result<RuntimeReferralRedeemRecord, SpacetimeClientError> {
let procedure_input =
build_runtime_referral_redeem_input(user_id, invite_code, updated_at_micros)
.map_err(|error| SpacetimeClientError::Runtime(error.to_string()))?
.into();
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.redeem_profile_referral_invite_code_then(procedure_input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_runtime_referral_redeem_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn get_profile_play_stats(
&self,
user_id: String,

View File

@@ -2653,6 +2653,7 @@ fn summarize_publish_gate_from_json(
&[
"worldHook",
"creatorIntent.worldHook",
"anchorContent.worldPromise",
"anchorContent.worldPromise.hook",
"settingText",
],
@@ -2670,6 +2671,7 @@ fn summarize_publish_gate_from_json(
&[
"playerPremise",
"creatorIntent.playerPremise",
"anchorContent.playerEntryPoint",
"anchorContent.playerEntryPoint.openingIdentity",
"anchorContent.playerEntryPoint.openingProblem",
"anchorContent.playerEntryPoint.entryMotivation",

View File

@@ -3766,6 +3766,7 @@ fn summarize_publish_gate_from_json(
&[
"worldHook",
"creatorIntent.worldHook",
"anchorContent.worldPromise",
"anchorContent.worldPromise.hook",
"settingText",
],
@@ -3783,6 +3784,7 @@ fn summarize_publish_gate_from_json(
&[
"playerPremise",
"creatorIntent.playerPremise",
"anchorContent.playerEntryPoint",
"anchorContent.playerEntryPoint.openingIdentity",
"anchorContent.playerEntryPoint.openingProblem",
"anchorContent.playerEntryPoint.entryMotivation",

View File

@@ -28,6 +28,34 @@ pub struct ProfileWalletLedger {
pub(crate) created_at: Timestamp,
}
#[spacetimedb::table(accessor = profile_invite_code)]
pub struct ProfileInviteCode {
#[primary_key]
pub(crate) user_id: String,
#[unique]
pub(crate) invite_code: String,
pub(crate) created_at: Timestamp,
pub(crate) updated_at: Timestamp,
}
#[spacetimedb::table(
accessor = profile_referral_relation,
index(accessor = by_profile_referral_inviter_user_id, btree(columns = [inviter_user_id])),
index(
accessor = by_profile_referral_inviter_bound_at,
btree(columns = [inviter_user_id, bound_at])
)
)]
pub struct ProfileReferralRelation {
#[primary_key]
pub(crate) invitee_user_id: String,
pub(crate) inviter_user_id: String,
pub(crate) invite_code: String,
pub(crate) inviter_reward_granted: bool,
pub(crate) invitee_reward_granted: bool,
pub(crate) bound_at: Timestamp,
}
#[spacetimedb::table(
accessor = profile_played_world,
index(accessor = by_profile_played_world_user_id, btree(columns = [user_id])),
@@ -274,6 +302,46 @@ pub fn create_profile_recharge_order_and_return(
}
}
// 邀请中心会在首次打开时为账号创建稳定邀请码,前端只展示这里返回的后端状态。
#[spacetimedb::procedure]
pub fn get_profile_referral_invite_center(
ctx: &mut ProcedureContext,
input: RuntimeReferralInviteCenterGetInput,
) -> RuntimeReferralInviteCenterProcedureResult {
match ctx.try_with_tx(|tx| get_profile_referral_invite_center_snapshot(tx, input.clone())) {
Ok(record) => RuntimeReferralInviteCenterProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeReferralInviteCenterProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
// 填码绑定、每日邀请者奖励上限和双方积分发放都在同一事务内完成。
#[spacetimedb::procedure]
pub fn redeem_profile_referral_invite_code(
ctx: &mut ProcedureContext,
input: RuntimeReferralRedeemInput,
) -> RuntimeReferralRedeemProcedureResult {
match ctx.try_with_tx(|tx| redeem_profile_referral_invite_code_record(tx, input.clone())) {
Ok(record) => RuntimeReferralRedeemProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeReferralRedeemProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
pub(crate) fn list_profile_save_archive_rows(
ctx: &ReducerContext,
input: RuntimeProfileSaveArchiveListInput,
@@ -948,6 +1016,215 @@ fn create_profile_recharge_order_record(
))
}
fn get_profile_referral_invite_center_snapshot(
ctx: &ReducerContext,
input: RuntimeReferralInviteCenterGetInput,
) -> Result<RuntimeReferralInviteCenterSnapshot, String> {
let validated_input = build_runtime_referral_invite_center_get_input(input.user_id)
.map_err(|error| error.to_string())?;
Ok(build_profile_referral_invite_center_snapshot(
ctx,
&validated_input.user_id,
))
}
fn redeem_profile_referral_invite_code_record(
ctx: &ReducerContext,
input: RuntimeReferralRedeemInput,
) -> Result<RuntimeReferralRedeemSnapshot, String> {
let validated_input = build_runtime_referral_redeem_input(
input.user_id,
input.invite_code,
input.updated_at_micros,
)
.map_err(|error| error.to_string())?;
let bound_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
let invitee_user_id = validated_input.user_id;
let invite_code = validated_input.invite_code;
if ctx
.db
.profile_referral_relation()
.invitee_user_id()
.find(&invitee_user_id)
.is_some()
{
return Err("每个用户最多只能填写一个邀请码".to_string());
}
let inviter_code = ctx
.db
.profile_invite_code()
.invite_code()
.find(&invite_code)
.ok_or_else(|| "邀请码不存在".to_string())?;
if inviter_code.user_id == invitee_user_id {
return Err("不能填写自己的邀请码".to_string());
}
let invitee_balance_after = apply_profile_wallet_delta(
ctx,
&invitee_user_id,
PROFILE_REFERRAL_REWARD_POINTS,
RuntimeProfileWalletLedgerSourceType::InviteInviteeReward,
&format!(
"invitee:{}:{}",
invitee_user_id, validated_input.updated_at_micros
),
bound_at,
)?;
let today_inviter_reward_count =
count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at);
let inviter_reward_granted =
today_inviter_reward_count < PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT;
let inviter_balance_after = if inviter_reward_granted {
apply_profile_wallet_delta(
ctx,
&inviter_code.user_id,
PROFILE_REFERRAL_REWARD_POINTS,
RuntimeProfileWalletLedgerSourceType::InviteInviterReward,
&format!(
"inviter:{}:{}",
inviter_code.user_id, validated_input.updated_at_micros
),
bound_at,
)?
} else {
profile_wallet_balance(ctx, &inviter_code.user_id)
};
ctx.db
.profile_referral_relation()
.insert(ProfileReferralRelation {
invitee_user_id: invitee_user_id.clone(),
inviter_user_id: inviter_code.user_id,
invite_code,
inviter_reward_granted,
invitee_reward_granted: true,
bound_at,
});
Ok(RuntimeReferralRedeemSnapshot {
center: build_profile_referral_invite_center_snapshot(ctx, &invitee_user_id),
invitee_reward_granted: true,
inviter_reward_granted,
invitee_balance_after,
inviter_balance_after,
})
}
fn build_profile_referral_invite_center_snapshot(
ctx: &ReducerContext,
user_id: &str,
) -> RuntimeReferralInviteCenterSnapshot {
let code = ensure_profile_invite_code(ctx, user_id);
let today_inviter_reward_count =
count_today_profile_referral_inviter_rewards(ctx, user_id, ctx.timestamp);
let invited_count = ctx
.db
.profile_referral_relation()
.iter()
.filter(|row| row.inviter_user_id == user_id)
.count() as u32;
let rewarded_invite_count = ctx
.db
.profile_referral_relation()
.iter()
.filter(|row| row.inviter_user_id == user_id && row.inviter_reward_granted)
.count() as u32;
let bound_relation = ctx
.db
.profile_referral_relation()
.invitee_user_id()
.find(&user_id.to_string());
RuntimeReferralInviteCenterSnapshot {
user_id: user_id.to_string(),
invite_code: code.invite_code.clone(),
invite_link_path: format!("/?inviteCode={}", code.invite_code),
invited_count,
rewarded_invite_count,
today_inviter_reward_count,
today_inviter_reward_remaining: PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT
.saturating_sub(today_inviter_reward_count),
reward_points: PROFILE_REFERRAL_REWARD_POINTS,
has_redeemed_code: bound_relation.is_some(),
bound_inviter_user_id: bound_relation
.as_ref()
.map(|relation| relation.inviter_user_id.clone()),
bound_at_micros: bound_relation
.as_ref()
.map(|relation| relation.bound_at.to_micros_since_unix_epoch()),
updated_at_micros: code.updated_at.to_micros_since_unix_epoch(),
}
}
fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInviteCode {
if let Some(row) = ctx
.db
.profile_invite_code()
.user_id()
.find(&user_id.to_string())
{
return row;
}
let mut invite_code = build_profile_invite_code(user_id, 0);
let mut salt = 1;
while ctx
.db
.profile_invite_code()
.invite_code()
.find(&invite_code)
.is_some()
{
invite_code = build_profile_invite_code(user_id, salt);
salt += 1;
}
ctx.db.profile_invite_code().insert(ProfileInviteCode {
user_id: user_id.to_string(),
invite_code,
created_at: ctx.timestamp,
updated_at: ctx.timestamp,
})
}
fn build_profile_invite_code(user_id: &str, salt: u32) -> String {
let mut hash = 14_695_981_039_346_656_037u64;
for byte in user_id.as_bytes().iter().copied().chain(salt.to_le_bytes()) {
hash ^= byte as u64;
hash = hash.wrapping_mul(1_099_511_628_211);
}
format!("SY{:08X}", hash as u32)
}
fn count_today_profile_referral_inviter_rewards(
ctx: &ReducerContext,
user_id: &str,
now: Timestamp,
) -> u32 {
let day_start_micros = (now.to_micros_since_unix_epoch() / 86_400_000_000) * 86_400_000_000;
ctx.db
.profile_wallet_ledger()
.iter()
.filter(|row| {
row.user_id == user_id
&& row.source_type == RuntimeProfileWalletLedgerSourceType::InviteInviterReward
&& row.created_at.to_micros_since_unix_epoch() >= day_start_micros
})
.count() as u32
}
fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
ctx.db
.profile_dashboard_state()
.user_id()
.find(&user_id.to_string())
.map(|row| row.wallet_balance)
.unwrap_or(0)
}
fn build_profile_recharge_center_snapshot(
ctx: &ReducerContext,
user_id: &str,