217 lines
7.6 KiB
Rust
217 lines
7.6 KiB
Rust
use crate::*;
|
||
|
||
#[spacetimedb::table(accessor = runtime_snapshot)]
|
||
pub struct RuntimeSnapshotRow {
|
||
#[primary_key]
|
||
pub(crate) user_id: String,
|
||
pub(crate) version: u32,
|
||
pub(crate) saved_at: Timestamp,
|
||
pub(crate) bottom_tab: String,
|
||
pub(crate) game_state_json: String,
|
||
pub(crate) current_story_json: Option<String>,
|
||
pub(crate) created_at: Timestamp,
|
||
pub(crate) updated_at: Timestamp,
|
||
}
|
||
|
||
// 当前快照读取保持旧 Node 语义:无快照时返回 ok=true + record=None,而不是默认空对象。
|
||
#[spacetimedb::procedure]
|
||
pub fn get_runtime_snapshot(
|
||
ctx: &mut ProcedureContext,
|
||
input: RuntimeSnapshotGetInput,
|
||
) -> RuntimeSnapshotProcedureResult {
|
||
match ctx.try_with_tx(|tx| get_runtime_snapshot_record(tx, input.clone())) {
|
||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||
ok: true,
|
||
record,
|
||
error_message: None,
|
||
},
|
||
Err(message) => RuntimeSnapshotProcedureResult {
|
||
ok: false,
|
||
record: None,
|
||
error_message: Some(message),
|
||
},
|
||
}
|
||
}
|
||
|
||
// PUT snapshot 主链会同步刷新 dashboard / wallet / played_world / save_archive 四类 projection。
|
||
#[spacetimedb::procedure]
|
||
pub fn upsert_runtime_snapshot_and_return(
|
||
ctx: &mut ProcedureContext,
|
||
input: RuntimeSnapshotUpsertInput,
|
||
) -> RuntimeSnapshotProcedureResult {
|
||
match ctx.try_with_tx(|tx| upsert_runtime_snapshot_record(tx, input.clone())) {
|
||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||
ok: true,
|
||
record: Some(record),
|
||
error_message: None,
|
||
},
|
||
Err(message) => RuntimeSnapshotProcedureResult {
|
||
ok: false,
|
||
record: None,
|
||
error_message: Some(message),
|
||
},
|
||
}
|
||
}
|
||
|
||
// 删除当前快照只影响 runtime_snapshot 主表,不联动清理 profile projection。
|
||
#[spacetimedb::procedure]
|
||
pub fn delete_runtime_snapshot_and_return(
|
||
ctx: &mut ProcedureContext,
|
||
input: RuntimeSnapshotDeleteInput,
|
||
) -> RuntimeSnapshotProcedureResult {
|
||
match ctx.try_with_tx(|tx| delete_runtime_snapshot_record(tx, input.clone())) {
|
||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||
ok: true,
|
||
record,
|
||
error_message: None,
|
||
},
|
||
Err(message) => RuntimeSnapshotProcedureResult {
|
||
ok: false,
|
||
record: None,
|
||
error_message: Some(message),
|
||
},
|
||
}
|
||
}
|
||
|
||
pub(crate) fn get_runtime_snapshot_record(
|
||
ctx: &ReducerContext,
|
||
input: RuntimeSnapshotGetInput,
|
||
) -> Result<Option<RuntimeSnapshot>, String> {
|
||
let validated_input =
|
||
build_runtime_snapshot_get_input(input.user_id).map_err(|error| error.to_string())?;
|
||
|
||
Ok(ctx
|
||
.db
|
||
.runtime_snapshot()
|
||
.user_id()
|
||
.find(&validated_input.user_id)
|
||
.map(|row| build_runtime_snapshot_from_row(&row)))
|
||
}
|
||
|
||
pub(crate) fn upsert_runtime_snapshot_record(
|
||
ctx: &ReducerContext,
|
||
input: RuntimeSnapshotUpsertInput,
|
||
) -> Result<RuntimeSnapshot, String> {
|
||
let current_story_value = parse_optional_json_str(input.current_story_json.as_deref())?;
|
||
let game_state = parse_json_str(&input.game_state_json)?;
|
||
let prepared = build_runtime_snapshot_upsert_input(
|
||
input.user_id,
|
||
input.saved_at_micros,
|
||
input.bottom_tab,
|
||
game_state.clone(),
|
||
current_story_value.clone(),
|
||
input.updated_at_micros,
|
||
)
|
||
.map_err(|error| error.to_string())?;
|
||
let updated_at = Timestamp::from_micros_since_unix_epoch(prepared.updated_at_micros);
|
||
let saved_at = Timestamp::from_micros_since_unix_epoch(prepared.saved_at_micros);
|
||
|
||
let snapshot = match ctx.db.runtime_snapshot().user_id().find(&prepared.user_id) {
|
||
Some(existing) => {
|
||
ctx.db
|
||
.runtime_snapshot()
|
||
.user_id()
|
||
.delete(&existing.user_id);
|
||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||
user_id: existing.user_id.clone(),
|
||
version: SAVE_SNAPSHOT_VERSION,
|
||
saved_at,
|
||
bottom_tab: prepared.bottom_tab.clone(),
|
||
game_state_json: prepared.game_state_json.clone(),
|
||
current_story_json: prepared.current_story_json.clone(),
|
||
created_at: existing.created_at,
|
||
updated_at,
|
||
});
|
||
|
||
RuntimeSnapshot {
|
||
user_id: existing.user_id,
|
||
version: SAVE_SNAPSHOT_VERSION,
|
||
saved_at_micros: prepared.saved_at_micros,
|
||
bottom_tab: prepared.bottom_tab,
|
||
game_state_json: prepared.game_state_json,
|
||
current_story_json: prepared.current_story_json,
|
||
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
||
updated_at_micros: prepared.updated_at_micros,
|
||
}
|
||
}
|
||
None => {
|
||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||
user_id: prepared.user_id.clone(),
|
||
version: SAVE_SNAPSHOT_VERSION,
|
||
saved_at,
|
||
bottom_tab: prepared.bottom_tab.clone(),
|
||
game_state_json: prepared.game_state_json.clone(),
|
||
current_story_json: prepared.current_story_json.clone(),
|
||
created_at: updated_at,
|
||
updated_at,
|
||
});
|
||
|
||
RuntimeSnapshot {
|
||
user_id: prepared.user_id,
|
||
version: SAVE_SNAPSHOT_VERSION,
|
||
saved_at_micros: prepared.saved_at_micros,
|
||
bottom_tab: prepared.bottom_tab,
|
||
game_state_json: prepared.game_state_json,
|
||
current_story_json: prepared.current_story_json,
|
||
created_at_micros: prepared.updated_at_micros,
|
||
updated_at_micros: prepared.updated_at_micros,
|
||
}
|
||
}
|
||
};
|
||
|
||
sync_profile_projections_from_snapshot(ctx, &snapshot)?;
|
||
|
||
Ok(snapshot)
|
||
}
|
||
|
||
pub(crate) fn delete_runtime_snapshot_record(
|
||
ctx: &ReducerContext,
|
||
input: RuntimeSnapshotDeleteInput,
|
||
) -> Result<Option<RuntimeSnapshot>, String> {
|
||
let validated_input =
|
||
build_runtime_snapshot_delete_input(input.user_id).map_err(|error| error.to_string())?;
|
||
|
||
let existing = ctx
|
||
.db
|
||
.runtime_snapshot()
|
||
.user_id()
|
||
.find(&validated_input.user_id);
|
||
if let Some(existing) = existing {
|
||
let snapshot = build_runtime_snapshot_from_row(&existing);
|
||
ctx.db
|
||
.runtime_snapshot()
|
||
.user_id()
|
||
.delete(&existing.user_id);
|
||
return Ok(Some(snapshot));
|
||
}
|
||
|
||
Ok(None)
|
||
}
|
||
|
||
pub(crate) fn build_runtime_snapshot_from_row(row: &RuntimeSnapshotRow) -> RuntimeSnapshot {
|
||
RuntimeSnapshot {
|
||
user_id: row.user_id.clone(),
|
||
version: row.version,
|
||
saved_at_micros: row.saved_at.to_micros_since_unix_epoch(),
|
||
bottom_tab: row.bottom_tab.clone(),
|
||
game_state_json: row.game_state_json.clone(),
|
||
current_story_json: row.current_story_json.clone(),
|
||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn parse_json_str(raw: &str) -> Result<JsonValue, String> {
|
||
serde_json::from_str::<JsonValue>(raw)
|
||
.map_err(|error| format!("game_state_json 解析失败: {error}"))
|
||
}
|
||
|
||
pub(crate) fn parse_optional_json_str(raw: Option<&str>) -> Result<Option<JsonValue>, String> {
|
||
match raw.map(str::trim).filter(|value| !value.is_empty()) {
|
||
Some(value) => serde_json::from_str::<JsonValue>(value)
|
||
.map(Some)
|
||
.map_err(|error| format!("current_story_json 解析失败: {error}")),
|
||
None => Ok(None),
|
||
}
|
||
}
|