feat: wire bark battle platform loop
Some checks are pending
CI / verify (pull_request) Waiting to run
Some checks are pending
CI / verify (pull_request) Waiting to run
This commit is contained in:
@@ -13,6 +13,7 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
module-ai = { workspace = true, features = ["spacetime-types"] }
|
||||
module-assets = { workspace = true, features = ["spacetime-types"] }
|
||||
module-bark-battle = { workspace = true }
|
||||
module-big-fish = { workspace = true, features = ["spacetime-types"] }
|
||||
module-combat = { workspace = true, features = ["spacetime-types"] }
|
||||
module-inventory = { workspace = true, features = ["spacetime-types"] }
|
||||
@@ -26,6 +27,7 @@ module-runtime = { workspace = true, features = ["spacetime-types"] }
|
||||
module-runtime-item = { workspace = true, features = ["spacetime-types"] }
|
||||
module-square-hole = { workspace = true }
|
||||
module-story = { workspace = true, features = ["spacetime-types"] }
|
||||
sha2 = { workspace = true }
|
||||
shared-kernel = { workspace = true }
|
||||
spacetimedb = { workspace = true, features = ["unstable"] }
|
||||
spacetimedb-lib = { workspace = true, features = ["serde"] }
|
||||
|
||||
872
server-rs/crates/spacetime-module/src/bark_battle/mod.rs
Normal file
872
server-rs/crates/spacetime-module/src/bark_battle/mod.rs
Normal file
@@ -0,0 +1,872 @@
|
||||
use crate::*;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub(crate) mod tables;
|
||||
mod types;
|
||||
|
||||
pub use tables::*;
|
||||
pub use types::*;
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn create_bark_battle_draft(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleDraftCreateInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| create_bark_battle_draft_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn update_bark_battle_draft_config(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleDraftConfigUpsertInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| update_bark_battle_draft_config_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn publish_bark_battle_work(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleWorkPublishInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| publish_bark_battle_work_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_bark_battle_runtime_config(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleRuntimeConfigGetInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_bark_battle_runtime_config_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn start_bark_battle_run(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleRunStartInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| start_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn finish_bark_battle_run(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleRunFinishInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| finish_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_bark_battle_run(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: BarkBattleRunGetInput,
|
||||
) -> BarkBattleProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_bark_battle_run_tx(tx, input.clone())) {
|
||||
Ok(snapshot) => bark_battle_json_result(&snapshot),
|
||||
Err(error) => bark_battle_error_result(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_bark_battle_draft_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleDraftCreateInput,
|
||||
) -> Result<BarkBattleDraftConfigSnapshot, String> {
|
||||
require_non_empty(&input.draft_id, "bark_battle draft_id")?;
|
||||
require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?;
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
if ctx
|
||||
.db
|
||||
.bark_battle_draft_config()
|
||||
.draft_id()
|
||||
.find(&input.draft_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("bark_battle_draft_config.draft_id 已存在".to_string());
|
||||
}
|
||||
|
||||
let now = Timestamp::from_micros_since_unix_epoch(input.created_at_micros);
|
||||
let config = BarkBattleEditorConfigSnapshot {
|
||||
title: normalize_title(input.title.as_deref())?,
|
||||
description: normalize_optional_text(input.description.as_deref()),
|
||||
theme_preset: normalize_required_preset(&input.theme_preset, "theme_preset")?,
|
||||
player_dog_skin_preset: normalize_required_preset(
|
||||
&input.player_dog_skin_preset,
|
||||
"player_dog_skin_preset",
|
||||
)?,
|
||||
opponent_dog_skin_preset: normalize_required_preset(
|
||||
&input.opponent_dog_skin_preset,
|
||||
"opponent_dog_skin_preset",
|
||||
)?,
|
||||
difficulty_preset: normalize_difficulty(input.difficulty_preset.as_deref())?,
|
||||
leaderboard_enabled: input.leaderboard_enabled.unwrap_or(true),
|
||||
};
|
||||
let row = BarkBattleDraftConfigRow {
|
||||
draft_id: input.draft_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
work_id: input.work_id.clone(),
|
||||
config_version: 1,
|
||||
ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(),
|
||||
difficulty_preset: config.difficulty_preset.clone(),
|
||||
leaderboard_enabled: config.leaderboard_enabled,
|
||||
config_json: to_json_string(&config),
|
||||
editor_state_json: normalize_json_string(
|
||||
input.editor_state_json.as_deref(),
|
||||
"editor_state_json",
|
||||
)?,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
ctx.db.bark_battle_draft_config().insert(row.clone());
|
||||
Ok(draft_snapshot(&row))
|
||||
}
|
||||
|
||||
fn update_bark_battle_draft_config_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleDraftConfigUpsertInput,
|
||||
) -> Result<BarkBattleDraftConfigSnapshot, String> {
|
||||
require_non_empty(&input.draft_id, "bark_battle draft_id")?;
|
||||
require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?;
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let editor_config = parse_editor_config(&input.config_json)?;
|
||||
validate_editor_config_snapshot(&editor_config)?;
|
||||
if editor_config.difficulty_preset != input.difficulty_preset
|
||||
|| editor_config.leaderboard_enabled != input.leaderboard_enabled
|
||||
{
|
||||
return Err("bark_battle config_json 与行字段不一致".to_string());
|
||||
}
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros);
|
||||
let existing = ctx
|
||||
.db
|
||||
.bark_battle_draft_config()
|
||||
.draft_id()
|
||||
.find(&input.draft_id)
|
||||
.ok_or_else(|| "bark_battle_draft_config.draft_id 不存在".to_string())?;
|
||||
if existing.owner_user_id != input.owner_user_id || existing.work_id != input.work_id {
|
||||
return Err("bark_battle draft owner/work 不匹配".to_string());
|
||||
}
|
||||
if input.config_version <= existing.config_version {
|
||||
return Err("bark_battle draft config_version 必须递增".to_string());
|
||||
}
|
||||
let mut row = existing;
|
||||
row.config_version = input.config_version;
|
||||
row.ruleset_version = normalize_ruleset_version(&input.ruleset_version)?;
|
||||
row.difficulty_preset = normalize_difficulty(Some(&input.difficulty_preset))?;
|
||||
row.leaderboard_enabled = input.leaderboard_enabled;
|
||||
row.config_json = input.config_json;
|
||||
row.updated_at = updated_at;
|
||||
ctx.db
|
||||
.bark_battle_draft_config()
|
||||
.draft_id()
|
||||
.update(row.clone());
|
||||
Ok(draft_snapshot(&row))
|
||||
}
|
||||
|
||||
fn publish_bark_battle_work_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleWorkPublishInput,
|
||||
) -> Result<BarkBattleRuntimeConfigSnapshot, String> {
|
||||
require_non_empty(&input.draft_id, "bark_battle draft_id")?;
|
||||
require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?;
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros);
|
||||
let draft = ctx
|
||||
.db
|
||||
.bark_battle_draft_config()
|
||||
.draft_id()
|
||||
.find(&input.draft_id)
|
||||
.ok_or_else(|| "bark_battle draft 不存在".to_string())?;
|
||||
if draft.owner_user_id != input.owner_user_id || draft.work_id != input.work_id {
|
||||
return Err("bark_battle draft owner/work 不匹配".to_string());
|
||||
}
|
||||
let published = BarkBattlePublishedConfigRow {
|
||||
work_id: draft.work_id.clone(),
|
||||
owner_user_id: draft.owner_user_id.clone(),
|
||||
source_draft_id: Some(draft.draft_id.clone()),
|
||||
config_version: draft.config_version,
|
||||
ruleset_version: normalize_ruleset_version(&draft.ruleset_version)?,
|
||||
difficulty_preset: normalize_difficulty(Some(&draft.difficulty_preset))?,
|
||||
leaderboard_enabled: draft.leaderboard_enabled,
|
||||
config_json: draft.config_json.clone(),
|
||||
published_snapshot_json: match input.published_snapshot_json.as_deref() {
|
||||
Some(value) => normalize_json_string(Some(value), "published_snapshot_json")?,
|
||||
None => draft.config_json.clone(),
|
||||
},
|
||||
created_at: published_at,
|
||||
updated_at: published_at,
|
||||
published_at,
|
||||
};
|
||||
let mut published = published;
|
||||
match ctx
|
||||
.db
|
||||
.bark_battle_published_config()
|
||||
.work_id()
|
||||
.find(&published.work_id)
|
||||
{
|
||||
Some(existing) => {
|
||||
published.created_at = existing.created_at;
|
||||
ctx.db
|
||||
.bark_battle_published_config()
|
||||
.work_id()
|
||||
.update(published.clone());
|
||||
}
|
||||
None => {
|
||||
ctx.db
|
||||
.bark_battle_published_config()
|
||||
.insert(published.clone());
|
||||
}
|
||||
}
|
||||
Ok(runtime_config_snapshot(&published))
|
||||
}
|
||||
|
||||
fn get_bark_battle_runtime_config_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleRuntimeConfigGetInput,
|
||||
) -> Result<BarkBattleRuntimeConfigSnapshot, String> {
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let row = ctx
|
||||
.db
|
||||
.bark_battle_published_config()
|
||||
.work_id()
|
||||
.find(&input.work_id)
|
||||
.ok_or_else(|| "bark_battle published config 不存在".to_string())?;
|
||||
if let Some(owner_user_id) = input.owner_user_id.as_deref() {
|
||||
if !owner_user_id.trim().is_empty() && row.owner_user_id != owner_user_id.trim() {
|
||||
return Err("bark_battle runtime config owner 不匹配".to_string());
|
||||
}
|
||||
}
|
||||
Ok(runtime_config_snapshot(&row))
|
||||
}
|
||||
|
||||
fn start_bark_battle_run_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleRunStartInput,
|
||||
) -> Result<BarkBattleRunSnapshot, String> {
|
||||
require_non_empty(&input.run_id, "bark_battle run_id")?;
|
||||
require_non_empty(&input.run_token, "bark_battle run_token")?;
|
||||
require_non_empty(&input.owner_user_id, "bark_battle owner_user_id")?;
|
||||
require_non_empty(&input.work_id, "bark_battle work_id")?;
|
||||
let published = ctx
|
||||
.db
|
||||
.bark_battle_published_config()
|
||||
.work_id()
|
||||
.find(&input.work_id)
|
||||
.ok_or_else(|| "bark_battle published config 不存在".to_string())?;
|
||||
if published.config_version != input.config_version
|
||||
|| published.ruleset_version != input.ruleset_version
|
||||
|| published.difficulty_preset != input.difficulty_preset
|
||||
{
|
||||
return Err("bark_battle run config/ruleset/difficulty 不匹配".to_string());
|
||||
}
|
||||
if ctx
|
||||
.db
|
||||
.bark_battle_runtime_run()
|
||||
.run_id()
|
||||
.find(&input.run_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("bark_battle run_id 已存在".to_string());
|
||||
}
|
||||
let started_at = ctx.timestamp;
|
||||
let row = BarkBattleRuntimeRunRow {
|
||||
run_id: input.run_id,
|
||||
run_token_hash: hash_run_token(&input.run_token),
|
||||
owner_user_id: input.owner_user_id,
|
||||
work_id: input.work_id,
|
||||
config_version: input.config_version,
|
||||
ruleset_version: input.ruleset_version,
|
||||
difficulty_preset: input.difficulty_preset,
|
||||
leaderboard_enabled: published.leaderboard_enabled,
|
||||
status: BARK_BATTLE_RUN_RUNNING.to_string(),
|
||||
client_started_at_micros: input.client_started_at_micros,
|
||||
server_started_at: started_at,
|
||||
client_finished_at_micros: None,
|
||||
server_finished_at: None,
|
||||
metrics_json: "{}".to_string(),
|
||||
server_result: None,
|
||||
validation_status: BARK_BATTLE_VALIDATION_PENDING.to_string(),
|
||||
anti_cheat_flags_json: "[]".to_string(),
|
||||
leaderboard_score: None,
|
||||
score_id: None,
|
||||
created_at: started_at,
|
||||
updated_at: started_at,
|
||||
};
|
||||
ctx.db.bark_battle_runtime_run().insert(row.clone());
|
||||
upsert_initial_work_stats(ctx, &row);
|
||||
Ok(run_snapshot(&row))
|
||||
}
|
||||
|
||||
fn finish_bark_battle_run_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleRunFinishInput,
|
||||
) -> Result<BarkBattleRunSnapshot, String> {
|
||||
require_non_empty(&input.run_id, "bark_battle run_id")?;
|
||||
require_non_empty(&input.run_token, "bark_battle run_token")?;
|
||||
let mut run = ctx
|
||||
.db
|
||||
.bark_battle_runtime_run()
|
||||
.run_id()
|
||||
.find(&input.run_id)
|
||||
.ok_or_else(|| "bark_battle run 不存在".to_string())?;
|
||||
if input.server_finished_at_micros > 0
|
||||
&& input.server_finished_at_micros < run.server_started_at.to_micros_since_unix_epoch()
|
||||
{
|
||||
return Err("bark_battle server_finished_at 早于 run start".to_string());
|
||||
}
|
||||
if ctx
|
||||
.timestamp
|
||||
.to_micros_since_unix_epoch()
|
||||
.saturating_sub(run.server_started_at.to_micros_since_unix_epoch())
|
||||
> 10 * 60 * 1_000_000
|
||||
{
|
||||
return Err("bark_battle run 已过期".to_string());
|
||||
}
|
||||
if run.run_token_hash != hash_run_token(&input.run_token) {
|
||||
return Err("bark_battle run_token 不匹配".to_string());
|
||||
}
|
||||
if run.status != BARK_BATTLE_RUN_RUNNING {
|
||||
return Err("bark_battle run 已结束".to_string());
|
||||
}
|
||||
if run.owner_user_id != input.owner_user_id
|
||||
|| run.work_id != input.work_id
|
||||
|| run.config_version != input.config_version
|
||||
|| run.ruleset_version != input.ruleset_version
|
||||
|| run.difficulty_preset != input.difficulty_preset
|
||||
{
|
||||
return Err("bark_battle finish identity/config 不匹配".to_string());
|
||||
}
|
||||
validate_json::<serde_json::Value>(&input.metrics_json, "metrics_json")?;
|
||||
validate_json::<serde_json::Value>(&input.derived_metrics_json, "derived_metrics_json")?;
|
||||
|
||||
let difficulty = parse_domain_difficulty(&input.difficulty_preset)?;
|
||||
let ruleset = module_bark_battle::BarkBattleRuleset::for_difficulty(difficulty);
|
||||
let finished_at = ctx.timestamp;
|
||||
let metrics = module_bark_battle::BarkBattleFinishMetrics {
|
||||
duration_ms: input.duration_ms,
|
||||
trigger_count: input.trigger_count,
|
||||
max_volume: millis_to_unit(input.max_volume_millis),
|
||||
average_volume: millis_to_unit(input.average_volume_millis),
|
||||
final_energy: millis_to_energy(input.final_energy_millis),
|
||||
max_combo: input.max_combo,
|
||||
finished_at_micros: finished_at.to_micros_since_unix_epoch(),
|
||||
};
|
||||
let validation = module_bark_battle::validate_finish_metrics(&ruleset, &metrics);
|
||||
let result = module_bark_battle::adjudicate_result(
|
||||
&ruleset,
|
||||
metrics.final_energy,
|
||||
millis_to_energy(input.opponent_final_energy_millis),
|
||||
);
|
||||
let leaderboard = if run.leaderboard_enabled {
|
||||
module_bark_battle::compute_leaderboard_score(&ruleset, &metrics, &validation, result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let leaderboard_score = leaderboard.map(compose_leaderboard_score);
|
||||
let score_id = format!("score-{}", input.run_id);
|
||||
let validation_status = validation_status_to_string(validation.decision);
|
||||
let server_result = battle_result_to_string(result);
|
||||
let flags_json = to_json_string(&validation.anti_cheat_flags);
|
||||
|
||||
ctx.db
|
||||
.bark_battle_score_record()
|
||||
.insert(BarkBattleScoreRecordRow {
|
||||
score_id: score_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
work_id: run.work_id.clone(),
|
||||
run_id: run.run_id.clone(),
|
||||
config_version: run.config_version,
|
||||
ruleset_version: run.ruleset_version.clone(),
|
||||
difficulty_preset: run.difficulty_preset.clone(),
|
||||
leaderboard_enabled: run.leaderboard_enabled,
|
||||
metrics_json: input.metrics_json.clone(),
|
||||
derived_metrics_json: input.derived_metrics_json.clone(),
|
||||
server_result: server_result.clone(),
|
||||
validation_status: validation_status.clone(),
|
||||
anti_cheat_flags_json: flags_json.clone(),
|
||||
leaderboard_score,
|
||||
recorded_at: finished_at,
|
||||
});
|
||||
|
||||
if let Some(score) = leaderboard_score {
|
||||
ctx.db
|
||||
.bark_battle_leaderboard_entry()
|
||||
.insert(BarkBattleLeaderboardEntryRow {
|
||||
leaderboard_entry_id: format!("leaderboard-{}", input.run_id),
|
||||
work_id: run.work_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
run_id: run.run_id.clone(),
|
||||
score_id: score_id.clone(),
|
||||
leaderboard_score: score,
|
||||
final_energy: metrics.final_energy,
|
||||
trigger_count: metrics.trigger_count,
|
||||
max_volume: metrics.max_volume,
|
||||
duration_closeness_ms: input.duration_ms.abs_diff(ruleset.standard_duration_ms),
|
||||
finished_at_micros: finished_at.to_micros_since_unix_epoch(),
|
||||
created_at: finished_at,
|
||||
updated_at: finished_at,
|
||||
});
|
||||
}
|
||||
|
||||
run.status = BARK_BATTLE_RUN_FINISHED.to_string();
|
||||
run.client_finished_at_micros = Some(input.client_finished_at_micros);
|
||||
run.server_finished_at = Some(finished_at);
|
||||
run.metrics_json = input.metrics_json;
|
||||
run.server_result = Some(server_result.clone());
|
||||
run.validation_status = validation_status.clone();
|
||||
run.anti_cheat_flags_json = flags_json;
|
||||
run.leaderboard_score = leaderboard_score;
|
||||
run.score_id = Some(score_id.clone());
|
||||
run.updated_at = finished_at;
|
||||
ctx.db
|
||||
.bark_battle_runtime_run()
|
||||
.run_id()
|
||||
.update(run.clone());
|
||||
upsert_finished_projections(
|
||||
ctx,
|
||||
&run,
|
||||
&score_id,
|
||||
leaderboard_score,
|
||||
metrics.final_energy,
|
||||
metrics.trigger_count,
|
||||
metrics.max_volume,
|
||||
input.duration_ms.abs_diff(ruleset.standard_duration_ms),
|
||||
&server_result,
|
||||
&validation_status,
|
||||
finished_at.to_micros_since_unix_epoch(),
|
||||
finished_at,
|
||||
);
|
||||
Ok(run_snapshot(&run))
|
||||
}
|
||||
|
||||
fn get_bark_battle_run_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: BarkBattleRunGetInput,
|
||||
) -> Result<BarkBattleRunSnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.bark_battle_runtime_run()
|
||||
.run_id()
|
||||
.find(&input.run_id)
|
||||
.ok_or_else(|| "bark_battle run 不存在".to_string())?;
|
||||
if row.owner_user_id != input.owner_user_id {
|
||||
return Err("bark_battle run owner 不匹配".to_string());
|
||||
}
|
||||
Ok(run_snapshot(&row))
|
||||
}
|
||||
|
||||
fn draft_snapshot(row: &BarkBattleDraftConfigRow) -> BarkBattleDraftConfigSnapshot {
|
||||
BarkBattleDraftConfigSnapshot {
|
||||
draft_id: row.draft_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
work_id: row.work_id.clone(),
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
config_json: row.config_json.clone(),
|
||||
editor_state_json: row.editor_state_json.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_config_snapshot(row: &BarkBattlePublishedConfigRow) -> BarkBattleRuntimeConfigSnapshot {
|
||||
BarkBattleRuntimeConfigSnapshot {
|
||||
work_id: row.work_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
source_draft_id: row.source_draft_id.clone(),
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
config_json: row.config_json.clone(),
|
||||
published_snapshot_json: row.published_snapshot_json.clone(),
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_run_token(token: &str) -> String {
|
||||
let digest = Sha256::digest(token.as_bytes());
|
||||
digest.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
fn normalize_json_string(value: Option<&str>, field_name: &str) -> Result<String, String> {
|
||||
let json = value.unwrap_or("{}").trim();
|
||||
validate_json::<serde_json::Value>(json, field_name)?;
|
||||
Ok(json.to_string())
|
||||
}
|
||||
|
||||
fn require_non_empty(value: &str, label: &str) -> Result<(), String> {
|
||||
if value.trim().is_empty() {
|
||||
Err(format!("{label} 不能为空"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_editor_config_snapshot(config: &BarkBattleEditorConfigSnapshot) -> Result<(), String> {
|
||||
normalize_title(Some(&config.title))?;
|
||||
normalize_required_preset(&config.theme_preset, "theme_preset")?;
|
||||
normalize_required_preset(&config.player_dog_skin_preset, "player_dog_skin_preset")?;
|
||||
normalize_required_preset(&config.opponent_dog_skin_preset, "opponent_dog_skin_preset")?;
|
||||
normalize_difficulty(Some(&config.difficulty_preset))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_title(value: Option<&str>) -> Result<String, String> {
|
||||
let title = value.unwrap_or("汪汪声浪挑战").trim();
|
||||
if title.is_empty() {
|
||||
return Err("bark_battle title 不能为空".to_string());
|
||||
}
|
||||
if title.chars().count() > 40 {
|
||||
return Err("bark_battle title 不能超过 40 个字符".to_string());
|
||||
}
|
||||
Ok(title.to_string())
|
||||
}
|
||||
|
||||
fn normalize_optional_text(value: Option<&str>) -> String {
|
||||
value.unwrap_or_default().trim().chars().take(120).collect()
|
||||
}
|
||||
|
||||
fn normalize_required_preset(value: &str, field_name: &str) -> Result<String, String> {
|
||||
let preset = value.trim();
|
||||
if preset.is_empty() {
|
||||
return Err(format!("bark_battle {field_name} 不能为空"));
|
||||
}
|
||||
Ok(preset.to_string())
|
||||
}
|
||||
|
||||
fn normalize_ruleset_version(value: &str) -> Result<String, String> {
|
||||
let ruleset = value.trim();
|
||||
if ruleset != BARK_BATTLE_DEFAULT_RULESET_VERSION {
|
||||
return Err("bark_battle ruleset_version 不支持".to_string());
|
||||
}
|
||||
Ok(ruleset.to_string())
|
||||
}
|
||||
|
||||
fn normalize_difficulty(value: Option<&str>) -> Result<String, String> {
|
||||
let difficulty = value.unwrap_or(BARK_BATTLE_DIFFICULTY_NORMAL).trim();
|
||||
match difficulty {
|
||||
BARK_BATTLE_DIFFICULTY_EASY
|
||||
| BARK_BATTLE_DIFFICULTY_NORMAL
|
||||
| BARK_BATTLE_DIFFICULTY_HARD => Ok(difficulty.to_string()),
|
||||
_ => Err("bark_battle difficulty_preset 不支持".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_editor_config(value: &str) -> Result<BarkBattleEditorConfigSnapshot, String> {
|
||||
serde_json::from_str::<BarkBattleEditorConfigSnapshot>(value)
|
||||
.map_err(|error| format!("bark_battle config_json JSON 无效: {error}"))
|
||||
}
|
||||
|
||||
fn validate_json<T: DeserializeOwned>(value: &str, field_name: &str) -> Result<(), String> {
|
||||
serde_json::from_str::<T>(value)
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("bark_battle {field_name} JSON 无效: {error}"))
|
||||
}
|
||||
|
||||
fn bark_battle_json_result<T: Serialize>(value: &T) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
row_json: Some(to_json_string(value)),
|
||||
error_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn bark_battle_error_result(error: String) -> BarkBattleProcedureResult {
|
||||
BarkBattleProcedureResult {
|
||||
ok: false,
|
||||
row_json: None,
|
||||
error_message: Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json_string<T: Serialize>(value: &T) -> String {
|
||||
serde_json::to_string(value).expect("serialize bark battle snapshot")
|
||||
}
|
||||
|
||||
fn run_snapshot(row: &BarkBattleRuntimeRunRow) -> BarkBattleRunSnapshot {
|
||||
BarkBattleRunSnapshot {
|
||||
run_id: row.run_id.clone(),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
work_id: row.work_id.clone(),
|
||||
config_version: row.config_version,
|
||||
ruleset_version: row.ruleset_version.clone(),
|
||||
difficulty_preset: row.difficulty_preset.clone(),
|
||||
leaderboard_enabled: row.leaderboard_enabled,
|
||||
status: row.status.clone(),
|
||||
client_started_at_micros: row.client_started_at_micros,
|
||||
server_started_at_micros: row.server_started_at.to_micros_since_unix_epoch(),
|
||||
client_finished_at_micros: row.client_finished_at_micros,
|
||||
server_finished_at_micros: row
|
||||
.server_finished_at
|
||||
.map(|t| t.to_micros_since_unix_epoch()),
|
||||
metrics_json: row.metrics_json.clone(),
|
||||
server_result: row.server_result.clone(),
|
||||
validation_status: row.validation_status.clone(),
|
||||
anti_cheat_flags_json: row.anti_cheat_flags_json.clone(),
|
||||
leaderboard_score: row.leaderboard_score,
|
||||
score_id: row.score_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn upsert_initial_work_stats(ctx: &ReducerContext, run: &BarkBattleRuntimeRunRow) {
|
||||
let now = run.created_at;
|
||||
match ctx
|
||||
.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.find(&run.work_id)
|
||||
{
|
||||
Some(mut stats) => {
|
||||
stats.play_count += 1;
|
||||
stats.updated_at = now;
|
||||
ctx.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.update(stats);
|
||||
}
|
||||
None => {
|
||||
ctx.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.insert(BarkBattleWorkStatsProjectionRow {
|
||||
work_id: run.work_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
play_count: 1,
|
||||
finished_count: 0,
|
||||
accepted_score_count: 0,
|
||||
leaderboard_entry_count: 0,
|
||||
best_leaderboard_score: None,
|
||||
best_score_id: None,
|
||||
best_run_id: None,
|
||||
average_final_energy: 0.0,
|
||||
average_trigger_count: 0.0,
|
||||
last_finished_at_micros: None,
|
||||
stats_json: "{}".to_string(),
|
||||
updated_at: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn upsert_finished_projections(
|
||||
ctx: &ReducerContext,
|
||||
run: &BarkBattleRuntimeRunRow,
|
||||
score_id: &str,
|
||||
leaderboard_score: Option<u64>,
|
||||
final_energy: f32,
|
||||
trigger_count: u64,
|
||||
max_volume: f32,
|
||||
duration_closeness_ms: u64,
|
||||
server_result: &str,
|
||||
validation_status: &str,
|
||||
finished_at_micros: i64,
|
||||
updated_at: Timestamp,
|
||||
) {
|
||||
let mut stats = ctx
|
||||
.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.find(&run.work_id)
|
||||
.unwrap_or_else(|| BarkBattleWorkStatsProjectionRow {
|
||||
work_id: run.work_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
play_count: 0,
|
||||
finished_count: 0,
|
||||
accepted_score_count: 0,
|
||||
leaderboard_entry_count: 0,
|
||||
best_leaderboard_score: None,
|
||||
best_score_id: None,
|
||||
best_run_id: None,
|
||||
average_final_energy: 0.0,
|
||||
average_trigger_count: 0.0,
|
||||
last_finished_at_micros: None,
|
||||
stats_json: "{}".to_string(),
|
||||
updated_at,
|
||||
});
|
||||
let previous_finished = stats.finished_count as f32;
|
||||
stats.finished_count += 1;
|
||||
if validation_status != BARK_BATTLE_VALIDATION_REJECTED {
|
||||
stats.accepted_score_count += 1;
|
||||
}
|
||||
if leaderboard_score.is_some() {
|
||||
stats.leaderboard_entry_count += 1;
|
||||
}
|
||||
stats.average_final_energy = ((stats.average_final_energy * previous_finished) + final_energy)
|
||||
/ stats.finished_count as f32;
|
||||
stats.average_trigger_count = ((stats.average_trigger_count * previous_finished)
|
||||
+ trigger_count as f32)
|
||||
/ stats.finished_count as f32;
|
||||
if leaderboard_score > stats.best_leaderboard_score {
|
||||
stats.best_leaderboard_score = leaderboard_score;
|
||||
stats.best_score_id = Some(score_id.to_string());
|
||||
stats.best_run_id = Some(run.run_id.clone());
|
||||
}
|
||||
stats.last_finished_at_micros = Some(finished_at_micros);
|
||||
stats.updated_at = updated_at;
|
||||
if ctx
|
||||
.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.find(&run.work_id)
|
||||
.is_some()
|
||||
{
|
||||
ctx.db
|
||||
.bark_battle_work_stats_projection()
|
||||
.work_id()
|
||||
.update(stats);
|
||||
} else {
|
||||
ctx.db.bark_battle_work_stats_projection().insert(stats);
|
||||
}
|
||||
|
||||
let personal_best_id = format!("{}:{}", run.owner_user_id, run.work_id);
|
||||
let should_update_best = validation_status != BARK_BATTLE_VALIDATION_REJECTED
|
||||
&& ctx
|
||||
.db
|
||||
.bark_battle_personal_best_projection()
|
||||
.personal_best_id()
|
||||
.find(&personal_best_id)
|
||||
.map(|best| {
|
||||
leaderboard_score > best.leaderboard_score || final_energy > best.final_energy
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if should_update_best {
|
||||
let row = BarkBattlePersonalBestProjectionRow {
|
||||
personal_best_id: personal_best_id.clone(),
|
||||
owner_user_id: run.owner_user_id.clone(),
|
||||
work_id: run.work_id.clone(),
|
||||
run_id: run.run_id.clone(),
|
||||
score_id: score_id.to_string(),
|
||||
leaderboard_entry_id: leaderboard_score.map(|_| format!("leaderboard-{}", run.run_id)),
|
||||
leaderboard_score,
|
||||
final_energy,
|
||||
trigger_count,
|
||||
max_volume,
|
||||
duration_closeness_ms,
|
||||
server_result: server_result.to_string(),
|
||||
validation_status: validation_status.to_string(),
|
||||
finished_at_micros,
|
||||
summary_json: "{}".to_string(),
|
||||
updated_at,
|
||||
};
|
||||
if ctx
|
||||
.db
|
||||
.bark_battle_personal_best_projection()
|
||||
.personal_best_id()
|
||||
.find(&personal_best_id)
|
||||
.is_some()
|
||||
{
|
||||
ctx.db
|
||||
.bark_battle_personal_best_projection()
|
||||
.personal_best_id()
|
||||
.update(row);
|
||||
} else {
|
||||
ctx.db.bark_battle_personal_best_projection().insert(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_domain_difficulty(value: &str) -> Result<module_bark_battle::DifficultyPreset, String> {
|
||||
match value {
|
||||
BARK_BATTLE_DIFFICULTY_EASY => Ok(module_bark_battle::DifficultyPreset::Easy),
|
||||
BARK_BATTLE_DIFFICULTY_NORMAL => Ok(module_bark_battle::DifficultyPreset::Normal),
|
||||
BARK_BATTLE_DIFFICULTY_HARD => Ok(module_bark_battle::DifficultyPreset::Hard),
|
||||
_ => Err("bark_battle difficulty_preset 不支持".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn millis_to_unit(value: u32) -> f32 {
|
||||
value as f32 / 1_000.0
|
||||
}
|
||||
|
||||
fn millis_to_energy(value: u32) -> f32 {
|
||||
value as f32 / 1_000.0
|
||||
}
|
||||
|
||||
fn validation_status_to_string(decision: module_bark_battle::FinishValidationDecision) -> String {
|
||||
match decision {
|
||||
module_bark_battle::FinishValidationDecision::Accepted => BARK_BATTLE_VALIDATION_ACCEPTED,
|
||||
module_bark_battle::FinishValidationDecision::AcceptedWithFlags => {
|
||||
BARK_BATTLE_VALIDATION_ACCEPTED_WITH_FLAGS
|
||||
}
|
||||
module_bark_battle::FinishValidationDecision::Rejected => BARK_BATTLE_VALIDATION_REJECTED,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn battle_result_to_string(result: module_bark_battle::BattleResult) -> String {
|
||||
match result {
|
||||
module_bark_battle::BattleResult::PlayerWin => "player_win",
|
||||
module_bark_battle::BattleResult::OpponentWin => "opponent_win",
|
||||
module_bark_battle::BattleResult::Draw => "draw",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn compose_leaderboard_score(score: module_bark_battle::BarkBattleLeaderboardScore) -> u64 {
|
||||
u64::from(score.final_energy_millis) * 10_000_000
|
||||
+ score.trigger_count.min(9_999)
|
||||
+ u64::from(score.max_volume_millis).min(999) * 10_000
|
||||
+ (10_000_u64.saturating_sub(score.duration_closeness_ms.min(10_000)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bark_battle_types_are_constructible() {
|
||||
let input = BarkBattleDraftConfigUpsertInput {
|
||||
draft_id: "draft-1".to_string(),
|
||||
owner_user_id: "user-1".to_string(),
|
||||
work_id: "work-1".to_string(),
|
||||
config_version: 1,
|
||||
ruleset_version: BARK_BATTLE_DEFAULT_RULESET_VERSION.to_string(),
|
||||
difficulty_preset: BARK_BATTLE_DIFFICULTY_NORMAL.to_string(),
|
||||
leaderboard_enabled: true,
|
||||
config_json: "{}".to_string(),
|
||||
updated_at_micros: 1_700_000,
|
||||
};
|
||||
|
||||
let result = BarkBattleProcedureResult {
|
||||
ok: true,
|
||||
row_json: Some(input.config_json.clone()),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
assert_eq!(input.draft_id, "draft-1");
|
||||
assert_eq!(input.ruleset_version, BARK_BATTLE_DEFAULT_RULESET_VERSION);
|
||||
assert!(result.ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_light_editor_config_before_publish() {
|
||||
assert_eq!(
|
||||
normalize_difficulty(Some(BARK_BATTLE_DIFFICULTY_HARD)).expect("difficulty"),
|
||||
BARK_BATTLE_DIFFICULTY_HARD
|
||||
);
|
||||
assert!(normalize_difficulty(Some("insane")).is_err());
|
||||
assert!(normalize_title(Some(" 标题 ")).is_ok());
|
||||
assert!(normalize_title(Some(" ")).is_err());
|
||||
}
|
||||
}
|
||||
172
server-rs/crates/spacetime-module/src/bark_battle/tables.rs
Normal file
172
server-rs/crates/spacetime-module/src/bark_battle/tables.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_draft_config,
|
||||
index(accessor = by_bark_battle_draft_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_bark_battle_draft_work_id, btree(columns = [work_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattleDraftConfigRow {
|
||||
#[primary_key]
|
||||
pub(crate) draft_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) config_version: u64,
|
||||
pub(crate) ruleset_version: String,
|
||||
pub(crate) difficulty_preset: String,
|
||||
pub(crate) leaderboard_enabled: bool,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) editor_state_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_published_config,
|
||||
index(accessor = by_bark_battle_published_owner_user_id, btree(columns = [owner_user_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattlePublishedConfigRow {
|
||||
#[primary_key]
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) source_draft_id: Option<String>,
|
||||
pub(crate) config_version: u64,
|
||||
pub(crate) ruleset_version: String,
|
||||
pub(crate) difficulty_preset: String,
|
||||
pub(crate) leaderboard_enabled: bool,
|
||||
pub(crate) config_json: String,
|
||||
pub(crate) published_snapshot_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_runtime_run,
|
||||
index(accessor = by_bark_battle_run_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_bark_battle_run_work_id, btree(columns = [work_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattleRuntimeRunRow {
|
||||
#[primary_key]
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) run_token_hash: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) config_version: u64,
|
||||
pub(crate) ruleset_version: String,
|
||||
pub(crate) difficulty_preset: String,
|
||||
pub(crate) leaderboard_enabled: bool,
|
||||
pub(crate) status: String,
|
||||
pub(crate) client_started_at_micros: i64,
|
||||
pub(crate) server_started_at: Timestamp,
|
||||
pub(crate) client_finished_at_micros: Option<i64>,
|
||||
pub(crate) server_finished_at: Option<Timestamp>,
|
||||
pub(crate) metrics_json: String,
|
||||
pub(crate) server_result: Option<String>,
|
||||
pub(crate) validation_status: String,
|
||||
pub(crate) anti_cheat_flags_json: String,
|
||||
pub(crate) leaderboard_score: Option<u64>,
|
||||
pub(crate) score_id: Option<String>,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_score_record,
|
||||
index(accessor = by_bark_battle_score_owner_user_id, btree(columns = [owner_user_id])),
|
||||
index(accessor = by_bark_battle_score_work_id, btree(columns = [work_id])),
|
||||
index(accessor = by_bark_battle_score_run_id, btree(columns = [run_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattleScoreRecordRow {
|
||||
#[primary_key]
|
||||
pub(crate) score_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) config_version: u64,
|
||||
pub(crate) ruleset_version: String,
|
||||
pub(crate) difficulty_preset: String,
|
||||
pub(crate) leaderboard_enabled: bool,
|
||||
pub(crate) metrics_json: String,
|
||||
pub(crate) derived_metrics_json: String,
|
||||
pub(crate) server_result: String,
|
||||
pub(crate) validation_status: String,
|
||||
pub(crate) anti_cheat_flags_json: String,
|
||||
pub(crate) leaderboard_score: Option<u64>,
|
||||
pub(crate) recorded_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_leaderboard_entry,
|
||||
index(accessor = by_bark_battle_leaderboard_work_score, btree(columns = [work_id, leaderboard_score])),
|
||||
index(accessor = by_bark_battle_leaderboard_owner_work, btree(columns = [owner_user_id, work_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattleLeaderboardEntryRow {
|
||||
#[primary_key]
|
||||
pub(crate) leaderboard_entry_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) score_id: String,
|
||||
pub(crate) leaderboard_score: u64,
|
||||
pub(crate) final_energy: f32,
|
||||
pub(crate) trigger_count: u64,
|
||||
pub(crate) max_volume: f32,
|
||||
pub(crate) duration_closeness_ms: u64,
|
||||
pub(crate) finished_at_micros: i64,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_work_stats_projection,
|
||||
index(accessor = by_bark_battle_work_stats_owner_user_id, btree(columns = [owner_user_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattleWorkStatsProjectionRow {
|
||||
#[primary_key]
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) play_count: u64,
|
||||
pub(crate) finished_count: u64,
|
||||
pub(crate) accepted_score_count: u64,
|
||||
pub(crate) leaderboard_entry_count: u64,
|
||||
pub(crate) best_leaderboard_score: Option<u64>,
|
||||
pub(crate) best_score_id: Option<String>,
|
||||
pub(crate) best_run_id: Option<String>,
|
||||
pub(crate) average_final_energy: f32,
|
||||
pub(crate) average_trigger_count: f32,
|
||||
pub(crate) last_finished_at_micros: Option<i64>,
|
||||
pub(crate) stats_json: String,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = bark_battle_personal_best_projection,
|
||||
index(accessor = by_bark_battle_personal_best_work_id, btree(columns = [work_id])),
|
||||
index(accessor = by_bark_battle_personal_best_owner_work, btree(columns = [owner_user_id, work_id]))
|
||||
)]
|
||||
#[derive(Clone)]
|
||||
pub struct BarkBattlePersonalBestProjectionRow {
|
||||
#[primary_key]
|
||||
pub(crate) personal_best_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) score_id: String,
|
||||
pub(crate) leaderboard_entry_id: Option<String>,
|
||||
pub(crate) leaderboard_score: Option<u64>,
|
||||
pub(crate) final_energy: f32,
|
||||
pub(crate) trigger_count: u64,
|
||||
pub(crate) max_volume: f32,
|
||||
pub(crate) duration_closeness_ms: u64,
|
||||
pub(crate) server_result: String,
|
||||
pub(crate) validation_status: String,
|
||||
pub(crate) finished_at_micros: i64,
|
||||
pub(crate) summary_json: String,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
177
server-rs/crates/spacetime-module/src/bark_battle/types.rs
Normal file
177
server-rs/crates/spacetime-module/src/bark_battle/types.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const BARK_BATTLE_DEFAULT_RULESET_VERSION: &str =
|
||||
module_bark_battle::BARK_BATTLE_RULESET_VERSION_V1;
|
||||
pub const BARK_BATTLE_DIFFICULTY_EASY: &str = "easy";
|
||||
pub const BARK_BATTLE_DIFFICULTY_NORMAL: &str = "normal";
|
||||
pub const BARK_BATTLE_DIFFICULTY_HARD: &str = "hard";
|
||||
|
||||
pub const BARK_BATTLE_RUN_PENDING: &str = "Pending";
|
||||
pub const BARK_BATTLE_RUN_RUNNING: &str = "Running";
|
||||
pub const BARK_BATTLE_RUN_FINISHED: &str = "Finished";
|
||||
pub const BARK_BATTLE_RUN_ABORTED: &str = "Aborted";
|
||||
|
||||
pub const BARK_BATTLE_VALIDATION_PENDING: &str = "pending";
|
||||
pub const BARK_BATTLE_VALIDATION_ACCEPTED: &str = "accepted";
|
||||
pub const BARK_BATTLE_VALIDATION_ACCEPTED_WITH_FLAGS: &str = "accepted_with_flags";
|
||||
pub const BARK_BATTLE_VALIDATION_REJECTED: &str = "rejected";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleDraftCreateInput {
|
||||
pub draft_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub difficulty_preset: Option<String>,
|
||||
pub leaderboard_enabled: Option<bool>,
|
||||
pub editor_state_json: Option<String>,
|
||||
pub created_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleDraftConfigUpsertInput {
|
||||
pub draft_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleWorkPublishInput {
|
||||
pub draft_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub published_snapshot_json: Option<String>,
|
||||
pub published_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleRuntimeConfigGetInput {
|
||||
pub work_id: String,
|
||||
pub owner_user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleRunStartInput {
|
||||
pub run_id: String,
|
||||
pub run_token: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub client_started_at_micros: i64,
|
||||
pub server_started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleRunFinishInput {
|
||||
pub run_id: String,
|
||||
pub run_token: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub client_finished_at_micros: i64,
|
||||
pub server_finished_at_micros: i64,
|
||||
pub duration_ms: u64,
|
||||
pub trigger_count: u64,
|
||||
pub max_volume_millis: u32,
|
||||
pub average_volume_millis: u32,
|
||||
pub final_energy_millis: u32,
|
||||
pub opponent_final_energy_millis: u32,
|
||||
pub max_combo: u32,
|
||||
pub metrics_json: String,
|
||||
pub derived_metrics_json: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleRunGetInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub struct BarkBattleProcedureResult {
|
||||
pub ok: bool,
|
||||
pub row_json: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleEditorConfigSnapshot {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub theme_preset: String,
|
||||
pub player_dog_skin_preset: String,
|
||||
pub opponent_dog_skin_preset: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleDraftConfigSnapshot {
|
||||
pub draft_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub editor_state_json: String,
|
||||
pub created_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleRuntimeConfigSnapshot {
|
||||
pub work_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub source_draft_id: Option<String>,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub config_json: String,
|
||||
pub published_snapshot_json: String,
|
||||
pub published_at_micros: i64,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BarkBattleRunSnapshot {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub work_id: String,
|
||||
pub config_version: u64,
|
||||
pub ruleset_version: String,
|
||||
pub difficulty_preset: String,
|
||||
pub leaderboard_enabled: bool,
|
||||
pub status: String,
|
||||
pub client_started_at_micros: i64,
|
||||
pub server_started_at_micros: i64,
|
||||
pub client_finished_at_micros: Option<i64>,
|
||||
pub server_finished_at_micros: Option<i64>,
|
||||
pub metrics_json: String,
|
||||
pub server_result: Option<String>,
|
||||
pub validation_status: String,
|
||||
pub anti_cheat_flags_json: String,
|
||||
pub leaderboard_score: Option<u64>,
|
||||
pub score_id: Option<String>,
|
||||
}
|
||||
@@ -23,6 +23,7 @@ pub use spacetimedb::{
|
||||
mod ai;
|
||||
mod asset_metadata;
|
||||
mod auth;
|
||||
mod bark_battle;
|
||||
mod big_fish;
|
||||
mod custom_world;
|
||||
mod domain_types;
|
||||
@@ -38,6 +39,7 @@ mod visual_novel;
|
||||
pub use ai::*;
|
||||
pub use asset_metadata::*;
|
||||
pub use auth::*;
|
||||
pub use bark_battle::*;
|
||||
pub use big_fish::*;
|
||||
pub use custom_world::*;
|
||||
pub use domain_types::*;
|
||||
|
||||
@@ -6,6 +6,11 @@ use spacetimedb::sats::de::serde::DeserializeWrapper;
|
||||
use spacetimedb::sats::ser::serde::SerializeWrapper;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::bark_battle::tables::{
|
||||
bark_battle_draft_config, bark_battle_leaderboard_entry, bark_battle_personal_best_projection,
|
||||
bark_battle_published_config, bark_battle_runtime_run, bark_battle_score_record,
|
||||
bark_battle_work_stats_projection,
|
||||
};
|
||||
use crate::big_fish::big_fish_runtime_run;
|
||||
use crate::match3d::tables::{
|
||||
match3d_agent_message, match3d_agent_session, match3d_runtime_run, match3d_work_profile,
|
||||
@@ -216,6 +221,13 @@ macro_rules! migration_tables {
|
||||
puzzle_event,
|
||||
puzzle_runtime_run,
|
||||
puzzle_leaderboard_entry,
|
||||
bark_battle_draft_config,
|
||||
bark_battle_published_config,
|
||||
bark_battle_runtime_run,
|
||||
bark_battle_score_record,
|
||||
bark_battle_leaderboard_entry,
|
||||
bark_battle_work_stats_projection,
|
||||
bark_battle_personal_best_projection,
|
||||
match3d_agent_session,
|
||||
match3d_agent_message,
|
||||
match3d_work_profile,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
pub mod analytics_date_dimension;
|
||||
pub mod creation_entry_config;
|
||||
mod browse_history;
|
||||
pub mod creation_entry_config;
|
||||
mod profile;
|
||||
mod settings;
|
||||
mod snapshots;
|
||||
|
||||
pub use analytics_date_dimension::*;
|
||||
pub use creation_entry_config::*;
|
||||
pub use browse_history::*;
|
||||
pub use creation_entry_config::*;
|
||||
pub use profile::*;
|
||||
pub use settings::*;
|
||||
pub use snapshots::*;
|
||||
|
||||
Reference in New Issue
Block a user