This commit is contained in:
2026-05-14 21:33:34 +08:00
193 changed files with 17051 additions and 1203 deletions

11
server-rs/Cargo.lock generated
View File

@@ -90,6 +90,7 @@ dependencies = [
"module-ai",
"module-assets",
"module-auth",
"module-bark-battle",
"module-big-fish",
"module-combat",
"module-creative-agent",
@@ -1766,6 +1767,14 @@ dependencies = [
"tracing",
]
[[package]]
name = "module-bark-battle"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "module-big-fish"
version = "0.1.0"
@@ -3137,6 +3146,7 @@ dependencies = [
"log",
"module-ai",
"module-assets",
"module-bark-battle",
"module-big-fish",
"module-combat",
"module-custom-world",
@@ -3152,6 +3162,7 @@ dependencies = [
"module-story",
"serde",
"serde_json",
"sha2",
"shared-kernel",
"spacetimedb",
"spacetimedb-lib",

View File

@@ -11,6 +11,7 @@ members = [
"crates/module-ai",
"crates/module-assets",
"crates/module-auth",
"crates/module-bark-battle",
"crates/module-big-fish",
"crates/module-combat",
"crates/module-creative-agent",
@@ -50,6 +51,7 @@ license = "UNLICENSED"
module-ai = { path = "crates/module-ai", default-features = false }
module-assets = { path = "crates/module-assets", default-features = false }
module-auth = { path = "crates/module-auth", default-features = false }
module-bark-battle = { path = "crates/module-bark-battle", default-features = false }
module-big-fish = { path = "crates/module-big-fish", default-features = false }
module-combat = { path = "crates/module-combat", default-features = false }
module-creative-agent = { path = "crates/module-creative-agent", default-features = false }

View File

@@ -17,6 +17,7 @@ module-ai = { workspace = true }
module-assets = { workspace = true, features = ["server-service"] }
module-auth = { workspace = true }
module-big-fish = { workspace = true }
module-bark-battle = { workspace = true }
module-combat = { workspace = true }
module-creative-agent = { workspace = true }
module-custom-world = { workspace = true }

View File

@@ -1,18 +0,0 @@
{
"provider": "ark",
"protocol": "responses",
"model": "deepseek-v3-2-251201",
"stream": false,
"attempt": 1,
"maxTokens": null,
"messages": [
{
"role": "system",
"content": "你是严格的世界草稿 JSON 生成器。\n只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。"
},
{
"role": "user",
"content": "请先根据下面的玩家设定创建一份“世界核心骨架”,后续我会分步骤生成角色名单、场景名单和详细档案。\n你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。\n这一步只保留世界顶层信息与一个开局归处占位不要输出 playableNpcs、storyNpcs、landmarks也不要展开人物、地图细节或多幕场景内容。\n玩家设定\n世界承诺{\"hook\":\"在失真的海图上追查一场被篡改的沉船事故。\"}\n玩家切入口{\"entryMotivation\":\"查清父亲沉船真相\",\"openingIdentity\":\"被停职返乡的守灯人\",\"openingProblem\":\"灯塔记录被人改写\"}\n\n输出 JSON 模板:\n{\n \"name\": \"世界名称\",\n \"subtitle\": \"世界副标题\",\n \"summary\": \"世界概述\",\n \"tone\": \"世界基调\",\n \"playerGoal\": \"玩家核心目标\",\n \"templateWorldType\": \"WUXIA|XIANXIA\",\n \"majorFactions\": [\"势力甲\", \"势力乙\"],\n \"coreConflicts\": [\"冲突甲\", \"冲突乙\"],\n \"attributeSchema\": {\n \"slots\": [\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" },\n { \"name\": \"维度名\" }\n ]\n },\n \"camp\": {\n \"name\": \"开局归处名称\",\n \"description\": \"这是玩家进入世界后的第一处落脚点描述\"\n }\n}\n\n要求\n- 所有生成文本都必须使用中文。\n- 这一步只输出顶层 10 个字段name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、attributeSchema、camp。\n- 这是一个完全独立的自定义世界;不要在任何正文里直接写出“武侠世界”“仙侠世界”等现成世界名。\n- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。\n- camp 只表示玩家开局时的落脚处占位,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念;不要在这一步生成开局场景任务、三幕事件或三幕背景。\n- 不要输出 playableNpcs、storyNpcs、landmarks、items也不要输出任何角色和地图细节。\n- majorFactions 保持 2 到 3 个coreConflicts 保持 2 到 3 个。\n- attributeSchema 必须是本世界专属的角色六维名称体系slots 必须恰好 6 个,每个 slot 只输出 name维度名必须是 2 到 4 个汉字且互不重复。\n- attributeSchema.slots 的 name 禁止使用:生命、法力、护甲、攻击、防御、力量、敏捷、智力、精神;不要写通用 DND 或传统四维属性。\n- 不要在 attributeSchema.slots 内输出 definition、positiveSignals、negativeSignals、combatUseText、socialUseText、explorationUseText 或其他说明字段。\n- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。\n- 每个字符串尽量简洁subtitle 控制在 8 到 18 个汉字内summary 控制在 16 到 32 个汉字内tone 控制在 6 到 16 个汉字内playerGoal 控制在 16 到 32 个汉字内camp.description 控制在 18 到 40 个汉字内。\n- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。"
}
]
}

View File

@@ -1 +0,0 @@
{"choices":[{"message":{"content":"{\"name\":\"雾港归航\",\"subtitle\":\"失灯旧案\",\"summary\":\"守灯人与群岛议会围绕沉船旧案对峙。\",\"tone\":\"海雾悬疑\",\"playerGoal\":\"查清父亲沉船真相\",\"templateWorldType\":\"WUXIA\",\"majorFactions\":[\"群岛议会\",\"灯塔署\"],\"coreConflicts\":[\"守灯塔的旧档案被人改写。\"],\"attributeSchema\":{\"slots\":[{\"name\":\"灯骨\"},{\"name\":\"潮步\"},{\"name\":\"灯识\"},{\"name\":\"雾魄\"},{\"name\":\"旧约\"},{\"name\":\"回澜\"}]},\"camp\":{\"name\":\"旧灯塔归舍\",\"description\":\"海雾边缘的守灯人旧居。\"}}"}}],"id":"resp_01"}

View File

@@ -1,18 +0,0 @@
{
"provider": "ark",
"protocol": "responses",
"model": "deepseek-v3-2-251201",
"stream": false,
"attempt": 1,
"maxTokens": null,
"messages": [
{
"role": "system",
"content": "你是严格的世界草稿 JSON 生成器。\n只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。"
},
{
"role": "user",
"content": "请为下面这一批场景角色补全养成档案。\n你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。\n世界核心信息\n世界雾港归航\n副标题失灯旧案\n世界概述守灯人与群岛议会围绕沉船旧案对峙。\n世界基调海雾悬疑\n玩家核心目标查清父亲沉船真相\n主要势力群岛议会、灯塔署\n核心冲突守灯塔的旧档案被人改写。\n开局归处旧灯塔归舍海雾边缘的守灯人旧居。\n关键场景旧灯塔雾中仍亮着错位灯火、沉船湾退潮后露出旧船骨\n本批角色\n- 议长甲 / 群岛议长\n身份遮掩者\n框架描述压住旧档的人\n预设好感-10\n关系切入口旧档案\n标签议会\n- 潮医乙 / 潮汐医师\n身份证人\n框架描述知道沉船伤痕\n预设好感20\n关系切入口救治记录\n标签证人\n出现场景沉船湾\n输出 JSON 模板:\n{\n \"storyNpcs\": [\n {\n \"name\": \"角色名称\",\n \"backstoryReveal\": { \"publicSummary\": \"公开摘要\", \"chapters\": [{ \"affinityRequired\": 15, \"title\": \"羁绊章节\", \"summary\": \"章节摘要\" }] },\n \"skills\": [{ \"name\": \"技能名\", \"summary\": \"技能摘要\", \"style\": \"风格\" }],\n \"initialItems\": [{ \"name\": \"物品名\", \"category\": \"道具\", \"quantity\": 1, \"rarity\": \"common\", \"description\": \"描述\", \"tags\": [\"标签\"] }]\n }\n ]\n}\n要求\n- 必须只补全本批角色name 必须与本批角色完全一致,不得增删改名。\n- 每个角色必须包含name、backstoryReveal、skills、initialItems。\n- backstoryReveal 必须包含 publicSummary 和 4 个 chapterschapters.affinityRequired 固定为 15、30、60、90。\n- skills 默认 3 个initialItems 默认 3 个;不要输出 backstory、personality、motivation、combatStyle。\n- 所有生成文本都必须使用中文。\n- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。"
}
]
}

View File

@@ -1 +0,0 @@
{"error":{"message":"story dossier timeout"}}

View File

@@ -1,18 +0,0 @@
{
"provider": "ark",
"protocol": "responses",
"model": "deepseek-v3-2-251201",
"stream": false,
"attempt": 1,
"maxTokens": null,
"messages": [
{
"role": "system",
"content": "你是严格的世界草稿 JSON 生成器。\n只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。"
},
{
"role": "user",
"content": "请为下面这一批场景角色补全养成档案。\n你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。\n世界核心信息\n世界雾港归航\n副标题失灯旧案\n世界概述守灯人与群岛议会围绕沉船旧案对峙。\n世界基调海雾悬疑\n玩家核心目标查清父亲沉船真相\n主要势力群岛议会、灯塔署\n核心冲突守灯塔的旧档案被人改写。\n开局归处旧灯塔归舍海雾边缘的守灯人旧居。\n关键场景旧灯塔雾中仍亮着错位灯火、沉船湾退潮后露出旧船骨\n本批角色\n- 雾商丙 / 雾港商人\n身份中间人\n框架描述贩卖航线的人\n预设好感5\n关系切入口伪造海图\n标签商人\n- 灯童丁 / 灯塔学徒\n身份目击者\n框架描述听见夜钟的人\n预设好感30\n关系切入口夜钟\n标签学徒\n出现场景旧灯塔\n输出 JSON 模板:\n{\n \"storyNpcs\": [\n {\n \"name\": \"角色名称\",\n \"backstoryReveal\": { \"publicSummary\": \"公开摘要\", \"chapters\": [{ \"affinityRequired\": 15, \"title\": \"羁绊章节\", \"summary\": \"章节摘要\" }] },\n \"skills\": [{ \"name\": \"技能名\", \"summary\": \"技能摘要\", \"style\": \"风格\" }],\n \"initialItems\": [{ \"name\": \"物品名\", \"category\": \"道具\", \"quantity\": 1, \"rarity\": \"common\", \"description\": \"描述\", \"tags\": [\"标签\"] }]\n }\n ]\n}\n要求\n- 必须只补全本批角色name 必须与本批角色完全一致,不得增删改名。\n- 每个角色必须包含name、backstoryReveal、skills、initialItems。\n- backstoryReveal 必须包含 publicSummary 和 4 个 chapterschapters.affinityRequired 固定为 15、30、60、90。\n- skills 默认 3 个initialItems 默认 3 个;不要输出 backstory、personality、motivation、combatStyle。\n- 所有生成文本都必须使用中文。\n- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。"
}
]
}

View File

@@ -1 +0,0 @@
{"error":{"message":"story dossier timeout"}}

View File

@@ -1,56 +0,0 @@
{
"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

@@ -725,7 +725,7 @@ fn parse_admin_database_table_rows_sql_response(
.ok_or_else(|| "SQL rows 字段格式非法".to_string())?;
let rows = row_values
.iter()
.map(|row| build_admin_database_table_row(row, &columns))
.map(|row| build_admin_database_table_row_for_table(table_name, row, &columns))
.collect::<Vec<_>>();
Ok(AdminDatabaseTableRowsResponse {
table_name: table_name.to_string(),
@@ -769,7 +769,15 @@ fn extract_sql_statement_columns(statement: &Value) -> Vec<String> {
}
fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatabaseTableRowPayload {
let raw = normalize_admin_database_value(row);
build_admin_database_table_row_for_table("", row, columns)
}
fn build_admin_database_table_row_for_table(
table_name: &str,
row: &Value,
columns: &[String],
) -> AdminDatabaseTableRowPayload {
let raw = normalize_admin_database_table_row_raw(table_name, row, columns);
let mut cells = Map::new();
if let Some(values) = row.as_array() {
for (index, value) in values.iter().enumerate() {
@@ -777,11 +785,17 @@ fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatab
.get(index)
.cloned()
.unwrap_or_else(|| format!("col_{}", index + 1));
cells.insert(key, normalize_admin_database_value(value));
cells.insert(
key.clone(),
normalize_admin_database_table_cell(table_name, &key, value),
);
}
} else if let Some(object) = row.as_object() {
for (key, value) in object {
cells.insert(key.clone(), normalize_admin_database_value(value));
cells.insert(
key.clone(),
normalize_admin_database_table_cell(table_name, key, value),
);
}
}
AdminDatabaseTableRowPayload {
@@ -790,6 +804,85 @@ fn build_admin_database_table_row(row: &Value, columns: &[String]) -> AdminDatab
}
}
fn normalize_admin_database_table_row_raw(
table_name: &str,
row: &Value,
columns: &[String],
) -> Value {
if let Some(values) = row.as_array() {
return Value::Array(
values
.iter()
.enumerate()
.map(|(index, value)| {
let key = columns.get(index).map(String::as_str).unwrap_or_default();
normalize_admin_database_table_cell(table_name, key, value)
})
.collect(),
);
}
if let Some(object) = row.as_object() {
return Value::Object(
object
.iter()
.map(|(key, value)| {
(
key.clone(),
normalize_admin_database_table_cell(table_name, key, value),
)
})
.collect(),
);
}
normalize_admin_database_value(row)
}
fn normalize_admin_database_table_cell(
table_name: &str,
column_name: &str,
value: &Value,
) -> Value {
if let Some(enum_value) = normalize_admin_database_known_enum(table_name, column_name, value) {
return enum_value;
}
normalize_admin_database_value(value)
}
fn normalize_admin_database_known_enum(
table_name: &str,
column_name: &str,
value: &Value,
) -> Option<Value> {
let variant_index = extract_sats_enum_variant_index(value)?;
let label = match (table_name, column_name) {
("profile_recharge_order", "kind") => match variant_index {
0 => "points",
1 => "membership",
_ => return None,
},
("profile_recharge_order", "status") => match variant_index {
0 => "pending",
1 => "paid",
2 => "failed",
3 => "closed",
4 => "refunded",
_ => return None,
},
_ => return None,
};
Some(Value::String(label.to_string()))
}
fn extract_sats_enum_variant_index(value: &Value) -> Option<u64> {
let items = value.as_array()?;
if items.len() != 2 {
return None;
}
items.first()?.as_u64()
}
fn normalize_admin_database_value(value: &Value) -> Value {
match value {
Value::Array(items) if items.len() == 1 => normalize_admin_database_value(&items[0]),
@@ -1526,6 +1619,46 @@ mod tests {
assert_eq!(response.rows[0].cells["points"], json!(12));
}
#[test]
fn parse_admin_database_table_rows_sql_response_maps_recharge_order_enum_cells() {
let payload = json!([
{
"schema": {
"elements": [
{"name": {"some": "order_id"}},
{"name": {"some": "kind"}},
{"name": {"some": "status"}},
{"name": {"some": "paid_at"}}
]
},
"rows": [[
"recharge:user_00000001:1778757456811099:points_60",
[0, []],
[0, []],
[1, []]
]]
}
]);
let response =
parse_admin_database_table_rows_sql_response("profile_recharge_order", 100, payload)
.expect("recharge order rows should parse");
let cells = &response.rows[0].cells;
assert_eq!(cells["kind"], json!("points"));
assert_eq!(cells["status"], json!("pending"));
assert_eq!(cells["paid_at"], json!(null));
assert_eq!(
response.rows[0].raw,
json!([
"recharge:user_00000001:1778757456811099:points_60",
"points",
"pending",
null
])
);
}
#[test]
fn build_admin_database_table_row_normalizes_optional_sats_values() {
let row = build_admin_database_table_row(

View File

@@ -34,6 +34,10 @@ use crate::{
auth_me::auth_me,
auth_public_user::{get_public_user_by_code, get_public_user_by_id},
auth_sessions::{auth_sessions, revoke_auth_session},
bark_battle::{
create_bark_battle_draft, finish_bark_battle_run, get_bark_battle_run,
get_bark_battle_runtime_config, publish_bark_battle_work, start_bark_battle_run,
},
big_fish::{
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
get_big_fish_session, get_big_fish_works, list_big_fish_gallery,
@@ -77,6 +81,8 @@ use crate::{
generate_custom_world_opening_cg, generate_custom_world_scene_image,
generate_custom_world_scene_npc, upload_custom_world_cover_image,
},
edutainment_baby_drawing::create_baby_love_drawing_magic,
edutainment_baby_object::generate_baby_object_match_assets,
error_middleware::normalize_error_response,
health::health_check,
hyper3d_generation::{
@@ -185,6 +191,7 @@ use crate::{
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
const PROFILE_FEEDBACK_BODY_LIMIT_BYTES: usize = 6 * 1024 * 1024;
const HYPER3D_IMAGE_TO_MODEL_BODY_LIMIT_BYTES: usize = 56 * 1024 * 1024;
const BABY_LOVE_DRAWING_MAGIC_BODY_LIMIT_BYTES: usize = 8 * 1024 * 1024;
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
pub fn build_router(state: AppState) -> Router {
@@ -648,6 +655,24 @@ pub fn build_router(state: AppState) -> Router {
"/api/creation-entry/config",
get(get_creation_entry_config_handler),
)
.route(
"/api/creation/edutainment/baby-object-match/assets",
post(generate_baby_object_match_assets).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/edutainment/baby-love-drawing/magic",
post(create_baby_love_drawing_magic)
.layer(DefaultBodyLimit::max(
BABY_LOVE_DRAWING_MAGIC_BODY_LIMIT_BYTES,
))
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/settings",
get(get_runtime_settings)
@@ -1055,6 +1080,48 @@ pub fn build_router(state: AppState) -> Router {
require_bearer_auth,
)),
)
.route(
"/api/creation/bark-battle/drafts",
post(create_bark_battle_draft).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/bark-battle/works/publish",
post(publish_bark_battle_work).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/bark-battle/works/{work_id}/config",
get(get_bark_battle_runtime_config).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/bark-battle/works/{work_id}/runs",
post(start_bark_battle_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/bark-battle/runs/{run_id}",
get(get_bark_battle_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/runtime/bark-battle/runs/{run_id}/finish",
post(finish_bark_battle_run).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/square-hole/sessions",
post(create_square_hole_agent_session).route_layer(middleware::from_fn_with_state(

View File

@@ -0,0 +1,776 @@
use std::time::{SystemTime, UNIX_EPOCH};
use axum::{
Json,
extract::{Extension, Path, State, rejection::JsonRejection},
http::{HeaderName, StatusCode, header},
response::Response,
};
use module_bark_battle::{BARK_BATTLE_RULESET_VERSION_V1, BarkBattleRuleset};
use serde::Deserialize;
use serde_json::{Value, json};
use shared_contracts::bark_battle::{
BarkBattleConfigEditorPayload, BarkBattleDerivedMetrics, BarkBattleDifficultyPreset,
BarkBattleDraftConfig, BarkBattleDraftCreateRequest, BarkBattleFinishStatus,
BarkBattlePublishedConfig, BarkBattleRunFinishRequest, BarkBattleRunFinishResponse,
BarkBattleRunStartRequest, BarkBattleRunStartResponse, BarkBattleScoreSummary,
BarkBattleServerResult, BarkBattleWorkPublishRequest,
};
use shared_kernel::{
build_prefixed_uuid_id, format_rfc3339, format_timestamp_micros,
offset_datetime_to_unix_micros, parse_rfc3339,
};
use spacetime_client::{
BarkBattleDraftCreateRecordInput, BarkBattleRunFinishRecordInput, BarkBattleRunRecord,
BarkBattleRunStartRecordInput, BarkBattleWorkPublishRecordInput, SpacetimeClientError,
};
use time::{Duration as TimeDuration, OffsetDateTime};
use crate::{
api_response::json_success_body,
auth::AuthenticatedAccessToken,
http_error::AppError,
request_context::RequestContext,
state::AppState,
work_play_tracking::{WorkPlayTrackingDraft, record_work_play_start_after_success},
};
const BARK_BATTLE_RUNTIME_PROVIDER: &str = "bark-battle-runtime";
const BARK_BATTLE_DRAFT_ID_PREFIX: &str = "bark-battle-draft-";
const BARK_BATTLE_WORK_ID_PREFIX: &str = "bark-battle-work-";
const BARK_BATTLE_RUN_ID_PREFIX: &str = "bark-battle-run-";
const BARK_BATTLE_RUN_TOKEN_PREFIX: &str = "bark-battle-token-";
const BARK_BATTLE_PLAY_TYPE_ID: &str = "bark-battle";
const BARK_BATTLE_RUN_TTL_SECONDS: i64 = 10 * 60;
#[derive(Clone, Debug, Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct BarkBattleRunSnapshotRecord {
run_id: String,
work_id: String,
config_version: u64,
ruleset_version: String,
difficulty_preset: String,
#[serde(default)]
client_started_at_micros: i64,
#[serde(default)]
server_started_at_micros: i64,
#[serde(default)]
server_finished_at_micros: Option<i64>,
#[serde(default)]
metrics_json: String,
#[serde(default)]
server_result: Option<String>,
#[serde(default)]
validation_status: String,
#[serde(default)]
anti_cheat_flags_json: String,
#[serde(default)]
leaderboard_score: Option<u64>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BarkBattleDraftConfigSnapshotRecord {
draft_id: String,
#[allow(dead_code)]
work_id: String,
#[allow(dead_code)]
config_version: u64,
#[allow(dead_code)]
ruleset_version: String,
#[serde(default)]
config_json: String,
updated_at_micros: i64,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BarkBattleRuntimeConfigSnapshotRecord {
work_id: String,
source_draft_id: Option<String>,
config_version: u64,
ruleset_version: String,
#[serde(default)]
config_json: String,
published_at_micros: i64,
updated_at_micros: i64,
}
pub async fn create_bark_battle_draft(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<BarkBattleDraftCreateRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = bark_battle_json(payload, &request_context)?;
let now = current_utc_micros();
let draft = state
.spacetime_client()
.create_bark_battle_draft(BarkBattleDraftCreateRecordInput {
draft_id: build_prefixed_uuid_id(BARK_BATTLE_DRAFT_ID_PREFIX),
owner_user_id: authenticated.claims().user_id().to_string(),
work_id: build_prefixed_uuid_id(BARK_BATTLE_WORK_ID_PREFIX),
title: Some(payload.title),
description: payload.description,
theme_preset: payload.theme_preset,
player_dog_skin_preset: payload.player_dog_skin_preset,
opponent_dog_skin_preset: payload.opponent_dog_skin_preset,
difficulty_preset: Some(
difficulty_to_spacetime_string(&payload.difficulty_preset).to_string(),
),
leaderboard_enabled: Some(payload.leaderboard_enabled),
editor_state_json: Some("{}".to_string()),
created_at_micros: now,
})
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let draft = map_draft_config_record(draft, &request_context)?;
Ok(json_success_body(Some(&request_context), draft))
}
pub async fn publish_bark_battle_work(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<BarkBattleWorkPublishRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = bark_battle_json(payload, &request_context)?;
ensure_non_empty(&request_context, &payload.draft_id, "draftId")?;
let work_id = payload
.work_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
.unwrap_or_else(|| build_prefixed_uuid_id(BARK_BATTLE_WORK_ID_PREFIX));
let published_snapshot_json = payload
.published_snapshot
.as_ref()
.map(serde_json::to_string)
.transpose()
.map_err(|error| {
bark_battle_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": format!("publishedSnapshot JSON 序列化失败: {error}"),
})),
)
})?;
let published = state
.spacetime_client()
.publish_bark_battle_work(BarkBattleWorkPublishRecordInput {
draft_id: payload.draft_id,
owner_user_id: authenticated.claims().user_id().to_string(),
work_id,
published_snapshot_json,
published_at_micros: current_utc_micros(),
})
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let published = map_published_config_record(published, &request_context)?;
Ok(json_success_body(Some(&request_context), published))
}
pub async fn get_bark_battle_runtime_config(
State(state): State<AppState>,
Path(work_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &work_id, "workId")?;
let config = state
.spacetime_client()
.get_bark_battle_runtime_config(work_id, Some(authenticated.claims().user_id().to_string()))
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let config = map_runtime_config_record(config, &request_context)?;
Ok(json_success_body(Some(&request_context), config))
}
pub async fn start_bark_battle_run(
State(state): State<AppState>,
Path(work_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<BarkBattleRunStartRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let maybe_payload = payload.ok().map(|Json(payload)| payload);
let request = maybe_payload.unwrap_or_else(|| BarkBattleRunStartRequest {
work_id: work_id.clone(),
config_version: None,
source_route: None,
client_runtime_version: None,
});
let work_id = if request.work_id.trim().is_empty() {
work_id
} else {
request.work_id.trim().to_string()
};
ensure_non_empty(&request_context, &work_id, "workId")?;
let owner_user_id = authenticated.claims().user_id().to_string();
let runtime_config = state
.spacetime_client()
.get_bark_battle_runtime_config(work_id.clone(), Some(owner_user_id.clone()))
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let runtime_config = map_runtime_config_record(runtime_config, &request_context)?;
if !request.work_id.trim().is_empty() && request.work_id.trim() != work_id {
return Err(bark_battle_bad_request(
&request_context,
"workId 与路径参数不一致",
));
}
if let Some(expected_version) = request.config_version {
if expected_version != runtime_config.config_version {
return Err(bark_battle_bad_request(
&request_context,
"configVersion 与已发布配置不一致",
));
}
}
let client_started_at_micros = current_utc_micros();
let run_token = build_prefixed_uuid_id(BARK_BATTLE_RUN_TOKEN_PREFIX);
let run = state
.spacetime_client()
.start_bark_battle_run(BarkBattleRunStartRecordInput {
run_id: build_prefixed_uuid_id(BARK_BATTLE_RUN_ID_PREFIX),
run_token: run_token.clone(),
owner_user_id: owner_user_id.clone(),
work_id: work_id.clone(),
config_version: u64::from(runtime_config.config_version),
ruleset_version: runtime_config.ruleset_version.clone(),
difficulty_preset: difficulty_to_spacetime_string(&runtime_config.difficulty_preset)
.to_string(),
client_started_at_micros,
server_started_at_micros: client_started_at_micros,
})
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let run_snapshot = parse_run_record(run, &request_context)?;
record_work_play_start_after_success(
&state,
&request_context,
WorkPlayTrackingDraft::new(
BARK_BATTLE_PLAY_TYPE_ID,
work_id.clone(),
&authenticated,
"/api/runtime/bark-battle/...",
)
.extra(json!({
"runId": run_snapshot.run_id,
"workId": work_id,
"configVersion": runtime_config.config_version,
"rulesetVersion": runtime_config.ruleset_version,
"difficultyPreset": runtime_config.difficulty_preset,
"sourceRoute": request.source_route,
"clientRuntimeVersion": request.client_runtime_version,
})),
)
.await;
let server_started_at = format_timestamp_micros(run_snapshot.server_started_at_micros);
let expires_at = format_timestamp_micros(
run_snapshot
.server_started_at_micros
.saturating_add(BARK_BATTLE_RUN_TTL_SECONDS * 1_000_000),
);
Ok(json_success_body(
Some(&request_context),
BarkBattleRunStartResponse {
run_id: run_snapshot.run_id,
run_token,
work_id: run_snapshot.work_id,
config_version: runtime_config.config_version,
ruleset_version: runtime_config.ruleset_version.clone(),
difficulty_preset: runtime_config.difficulty_preset.clone(),
runtime_config,
server_started_at,
expires_at,
},
))
}
pub async fn get_bark_battle_run(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &run_id, "runId")?;
let run = state
.spacetime_client()
.get_bark_battle_run(run_id, authenticated.claims().user_id().to_string())
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let run = parse_run_record(run, &request_context)?;
Ok(json_success_body(Some(&request_context), run))
}
pub async fn finish_bark_battle_run(
State(state): State<AppState>,
Path(run_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
payload: Result<Json<BarkBattleRunFinishRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = bark_battle_json(payload, &request_context)?;
ensure_non_empty(&request_context, &run_id, "runId")?;
ensure_non_empty(&request_context, &payload.work_id, "workId")?;
ensure_non_empty(&request_context, &payload.run_token, "runToken")?;
if payload.run_id != run_id {
return Err(bark_battle_bad_request(
&request_context,
"runId 与路径参数不一致",
));
}
if payload.ruleset_version != BARK_BATTLE_RULESET_VERSION_V1 {
return Err(bark_battle_bad_request(
&request_context,
"rulesetVersion 不支持",
));
}
let client_finished_at_micros = parse_client_time_to_micros(&payload.client_finished_at)
.map_err(|message| bark_battle_bad_request(&request_context, &message))?;
let derived = &payload.derived_metrics;
let opponent_final_energy = derive_server_opponent_final_energy(derived);
let metrics_json = serde_json::to_string(&json!({
"clientStartedAt": payload.client_started_at,
"clientFinishedAt": payload.client_finished_at,
"durationMs": payload.duration_ms,
"derivedMetrics": payload.derived_metrics,
"clientResult": payload.client_result,
"sampleDigest": payload.sample_digest,
"clientRuntimeVersion": payload.client_runtime_version,
}))
.unwrap_or_else(|_| "{}".to_string());
let derived_metrics_json = serde_json::to_string(derived).unwrap_or_else(|_| "{}".to_string());
let run = state
.spacetime_client()
.finish_bark_battle_run(BarkBattleRunFinishRecordInput {
run_id,
run_token: payload.run_token,
owner_user_id: authenticated.claims().user_id().to_string(),
work_id: payload.work_id.clone(),
config_version: u64::from(payload.config_version),
ruleset_version: payload.ruleset_version.clone(),
difficulty_preset: difficulty_to_spacetime_string(&payload.difficulty_preset)
.to_string(),
client_finished_at_micros,
server_finished_at_micros: current_utc_micros(),
duration_ms: payload.duration_ms,
trigger_count: u64::from(derived.trigger_count),
max_volume_millis: unit_to_millis(derived.max_volume),
average_volume_millis: unit_to_millis(derived.average_volume),
final_energy_millis: energy_to_millis(derived.final_energy),
opponent_final_energy_millis: energy_to_millis(opponent_final_energy),
max_combo: derived.combo_max,
metrics_json,
derived_metrics_json,
})
.await
.map_err(|error| {
bark_battle_error_response(&request_context, map_bark_battle_client_error(error))
})?;
let run = parse_run_record(run, &request_context)?;
Ok(json_success_body(
Some(&request_context),
map_finish_response(run, &payload.derived_metrics),
))
}
fn map_finish_response(
run: BarkBattleRunSnapshotRecord,
fallback_metrics: &BarkBattleDerivedMetrics,
) -> BarkBattleRunFinishResponse {
let score_summary =
parse_score_summary(&run.metrics_json).unwrap_or_else(|| BarkBattleScoreSummary {
duration_ms: 0,
trigger_count: fallback_metrics.trigger_count,
max_volume: fallback_metrics.max_volume,
average_volume: fallback_metrics.average_volume,
final_energy: fallback_metrics.final_energy,
combo_max: fallback_metrics.combo_max,
});
BarkBattleRunFinishResponse {
status: parse_finish_status(&run.validation_status),
run_id: run.run_id,
work_id: run.work_id,
config_version: run.config_version.min(u64::from(u32::MAX)) as u32,
ruleset_version: run.ruleset_version,
difficulty_preset: parse_difficulty_lossy(&run.difficulty_preset),
server_result: parse_server_result_lossy(run.server_result.as_deref()),
score_summary,
leaderboard_score: run.leaderboard_score,
anti_cheat_flags: parse_string_vec(&run.anti_cheat_flags_json),
updated_at: format_timestamp_micros(
run.server_finished_at_micros
.unwrap_or(run.server_started_at_micros),
),
}
}
fn parse_run_record(
value: BarkBattleRunRecord,
request_context: &RequestContext,
) -> Result<BarkBattleRunSnapshotRecord, Response> {
serde_json::from_value(value).map_err(|error| {
bark_battle_error_response(
request_context,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": format!("Bark Battle run JSON 解析失败: {error}"),
})),
)
})
}
fn parse_draft_snapshot_record(
value: Value,
request_context: &RequestContext,
) -> Result<BarkBattleDraftConfigSnapshotRecord, Response> {
serde_json::from_value(value)
.map_err(|error| bark_battle_snapshot_parse_error(request_context, "draft config", error))
}
fn parse_runtime_snapshot_record(
value: Value,
request_context: &RequestContext,
) -> Result<BarkBattleRuntimeConfigSnapshotRecord, Response> {
serde_json::from_value(value)
.map_err(|error| bark_battle_snapshot_parse_error(request_context, "runtime config", error))
}
fn map_draft_config_record(
value: Value,
request_context: &RequestContext,
) -> Result<BarkBattleDraftConfig, Response> {
let snapshot = parse_draft_snapshot_record(value, request_context)?;
let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?;
Ok(BarkBattleDraftConfig {
draft_id: snapshot.draft_id,
title: editor_config.title,
description: editor_config.description,
theme_preset: editor_config.theme_preset,
player_dog_skin_preset: editor_config.player_dog_skin_preset,
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
difficulty_preset: editor_config.difficulty_preset,
leaderboard_enabled: editor_config.leaderboard_enabled,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn map_runtime_config_record(
value: Value,
request_context: &RequestContext,
) -> Result<shared_contracts::bark_battle::BarkBattleRuntimeConfig, Response> {
let snapshot = parse_runtime_snapshot_record(value, request_context)?;
let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?;
let ruleset = BarkBattleRuleset::v1();
Ok(shared_contracts::bark_battle::BarkBattleRuntimeConfig {
work_id: snapshot.work_id,
config_version: snapshot.config_version.min(u64::from(u32::MAX)) as u32,
ruleset_version: snapshot.ruleset_version,
play_type_id: BARK_BATTLE_PLAY_TYPE_ID.to_string(),
duration_ms: ruleset.standard_duration_ms,
energy_min: 0.0,
energy_max: 100.0,
draw_threshold: ruleset.draw_threshold_energy as f32,
min_bark_gap_ms: ruleset.min_bark_gap_ms,
difficulty_preset: editor_config.difficulty_preset,
theme_preset: editor_config.theme_preset,
player_dog_skin_preset: editor_config.player_dog_skin_preset,
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
leaderboard_enabled: editor_config.leaderboard_enabled,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
})
}
fn map_published_config_record(
value: Value,
request_context: &RequestContext,
) -> Result<BarkBattlePublishedConfig, Response> {
let snapshot = parse_runtime_snapshot_record(value, request_context)?;
let editor_config = parse_editor_config_record(&snapshot.config_json, request_context)?;
Ok(BarkBattlePublishedConfig {
work_id: snapshot.work_id,
draft_id: snapshot.source_draft_id,
config_version: snapshot.config_version.min(u64::from(u32::MAX)) as u32,
ruleset_version: snapshot.ruleset_version,
play_type_id: BARK_BATTLE_PLAY_TYPE_ID.to_string(),
title: editor_config.title,
description: editor_config.description,
theme_preset: editor_config.theme_preset,
player_dog_skin_preset: editor_config.player_dog_skin_preset,
opponent_dog_skin_preset: editor_config.opponent_dog_skin_preset,
difficulty_preset: editor_config.difficulty_preset,
leaderboard_enabled: editor_config.leaderboard_enabled,
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
published_at: format_timestamp_micros(snapshot.published_at_micros),
})
}
fn parse_editor_config_record(
config_json: &str,
request_context: &RequestContext,
) -> Result<BarkBattleConfigEditorPayload, Response> {
serde_json::from_str(config_json).map_err(|error| {
bark_battle_error_response(
request_context,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": format!("Bark Battle configJson 解析失败: {error}"),
})),
)
})
}
fn bark_battle_snapshot_parse_error(
request_context: &RequestContext,
label: &str,
error: serde_json::Error,
) -> Response {
bark_battle_error_response(
request_context,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": format!("Bark Battle {label} JSON 解析失败: {error}"),
})),
)
}
fn bark_battle_json<T>(
payload: Result<Json<T>, JsonRejection>,
request_context: &RequestContext,
) -> Result<Json<T>, Response> {
payload.map_err(|error| {
bark_battle_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": error.body_text(),
})),
)
})
}
fn ensure_non_empty(
request_context: &RequestContext,
value: &str,
field_name: &str,
) -> Result<(), Response> {
if value.trim().is_empty() {
return Err(bark_battle_bad_request(
request_context,
&format!("{field_name} is required"),
));
}
Ok(())
}
fn bark_battle_bad_request(request_context: &RequestContext, message: &str) -> Response {
bark_battle_error_response(
request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": message,
})),
)
}
fn map_bark_battle_client_error(error: SpacetimeClientError) -> AppError {
let status = match &error {
SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST,
SpacetimeClientError::Procedure(message)
if message.contains("不存在")
|| message.contains("not found")
|| message.contains("does not exist") =>
{
StatusCode::NOT_FOUND
}
SpacetimeClientError::Procedure(message)
if message.contains("不能为空")
|| message.contains("不匹配")
|| message.contains("不支持")
|| message.contains("已结束")
|| message.contains("已存在") =>
{
StatusCode::BAD_REQUEST
}
_ => StatusCode::BAD_GATEWAY,
};
AppError::from_status(status).with_details(json!({
"provider": "spacetimedb",
"message": error.to_string(),
}))
}
fn bark_battle_error_response(request_context: &RequestContext, error: AppError) -> Response {
let mut response = error.into_response_with_context(Some(request_context));
response.headers_mut().insert(
HeaderName::from_static("x-genarrative-provider"),
header::HeaderValue::from_static(BARK_BATTLE_RUNTIME_PROVIDER),
);
response
}
fn parse_client_time_to_micros(value: &str) -> Result<i64, String> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err("client timestamp is required".to_string());
}
if let Ok(micros) = trimmed.parse::<i64>() {
return Ok(micros);
}
parse_rfc3339(trimmed).map(offset_datetime_to_unix_micros)
}
fn current_utc_micros() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
(duration.as_secs() as i64) * 1_000_000 + i64::from(duration.subsec_micros())
}
fn unit_to_millis(value: f32) -> u32 {
(value.clamp(0.0, 1.0) * 1_000.0).round() as u32
}
fn energy_to_millis(value: f32) -> u32 {
(value.clamp(0.0, 100.0) * 1_000.0).round() as u32
}
fn derive_server_opponent_final_energy(metrics: &BarkBattleDerivedMetrics) -> f32 {
let ruleset = BarkBattleRuleset::v1();
let pressure = (metrics.average_volume * 24.0)
+ (metrics.max_volume * 16.0)
+ (metrics.trigger_count as f32 * 0.35)
+ (metrics.combo_max as f32 * 0.2);
(ruleset.max_final_energy - pressure).clamp(ruleset.min_final_energy, ruleset.max_final_energy)
}
fn difficulty_to_spacetime_string(value: &BarkBattleDifficultyPreset) -> &'static str {
match value {
BarkBattleDifficultyPreset::Easy => "easy",
BarkBattleDifficultyPreset::Normal => "normal",
BarkBattleDifficultyPreset::Hard => "hard",
}
}
fn parse_difficulty(value: &str) -> Result<BarkBattleDifficultyPreset, AppError> {
match value {
"easy" => Ok(BarkBattleDifficultyPreset::Easy),
"normal" => Ok(BarkBattleDifficultyPreset::Normal),
"hard" => Ok(BarkBattleDifficultyPreset::Hard),
_ => Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": BARK_BATTLE_RUNTIME_PROVIDER,
"message": format!("Bark Battle difficultyPreset 不支持: {value}"),
})),
),
}
}
fn parse_difficulty_lossy(value: &str) -> BarkBattleDifficultyPreset {
parse_difficulty(value).unwrap_or(BarkBattleDifficultyPreset::Normal)
}
fn parse_finish_status(value: &str) -> BarkBattleFinishStatus {
match value {
"accepted" => BarkBattleFinishStatus::Accepted,
"accepted_with_flags" => BarkBattleFinishStatus::AcceptedWithFlags,
"rejected" => BarkBattleFinishStatus::Rejected,
_ => BarkBattleFinishStatus::Rejected,
}
}
fn parse_server_result_lossy(value: Option<&str>) -> BarkBattleServerResult {
match value {
Some("player_win") => BarkBattleServerResult::PlayerWin,
Some("opponent_win") => BarkBattleServerResult::OpponentWin,
Some("draw") => BarkBattleServerResult::Draw,
_ => BarkBattleServerResult::Draw,
}
}
fn parse_score_summary(metrics_json: &str) -> Option<BarkBattleScoreSummary> {
let value: Value = serde_json::from_str(metrics_json).ok()?;
let derived = value.get("derivedMetrics")?;
Some(BarkBattleScoreSummary {
duration_ms: value.get("durationMs")?.as_u64()?,
trigger_count: derived
.get("triggerCount")?
.as_u64()?
.min(u64::from(u32::MAX)) as u32,
max_volume: derived.get("maxVolume")?.as_f64()? as f32,
average_volume: derived.get("averageVolume")?.as_f64()? as f32,
final_energy: derived.get("finalEnergy")?.as_f64()? as f32,
combo_max: derived.get("comboMax")?.as_u64()?.min(u64::from(u32::MAX)) as u32,
})
}
fn parse_string_vec(value: &str) -> Vec<String> {
serde_json::from_str(value).unwrap_or_default()
}
#[allow(dead_code)]
fn format_rfc3339_or_timestamp_micros(micros: i64) -> String {
let seconds = micros.div_euclid(1_000_000);
let subsec_micros = micros.rem_euclid(1_000_000);
let Ok(value) = OffsetDateTime::from_unix_timestamp(seconds)
.map(|value| value + TimeDuration::microseconds(subsec_micros))
else {
return format_timestamp_micros(micros);
};
format_rfc3339(value).unwrap_or_else(|_| format_timestamp_micros(micros))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_and_energy_are_clamped_to_spacetime_millis() {
assert_eq!(unit_to_millis(0.625), 625);
assert_eq!(unit_to_millis(3.0), 1000);
assert_eq!(energy_to_millis(88.456), 88_456);
assert_eq!(energy_to_millis(120.0), 100_000);
}
#[test]
fn parses_rfc3339_and_numeric_client_timestamps() {
assert_eq!(
parse_client_time_to_micros("1713686401234567").unwrap(),
1_713_686_401_234_567
);
assert_eq!(
parse_client_time_to_micros("2024-04-21T04:00:01.234567Z").unwrap(),
1_713_672_001_234_567
);
}
}

View File

@@ -78,6 +78,9 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
if normalized.starts_with("/api/runtime/match3d") {
return Some("match3d");
}
if normalized.starts_with("/api/runtime/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/runtime/square-hole") {
return Some("square-hole");
}
@@ -90,6 +93,12 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
if normalized.starts_with("/api/creation/visual-novel") {
return Some("visual-novel");
}
if normalized.starts_with("/api/creation/edutainment/baby-object-match") {
return Some("baby-object-match");
}
if normalized.starts_with("/api/creation/edutainment/baby-love-drawing") {
return Some("baby-love-drawing");
}
None
}
@@ -112,40 +121,11 @@ pub(crate) fn test_creation_entry_config_response()
title: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
description: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
},
creation_types: vec![
test_creation_type("rpg", false, true, 10),
test_creation_type("big-fish", false, true, 20),
test_creation_type("puzzle", true, true, 30),
test_creation_type("match3d", true, true, 40),
test_creation_type("square-hole", false, true, 50),
test_creation_type("visual-novel", false, false, 60),
test_creation_type("airp", true, false, 70),
test_creation_type("creative-agent", false, true, 80),
],
creation_types: module_runtime::default_creation_entry_type_snapshots(0),
updated_at_micros: 0,
})
}
#[cfg(test)]
fn test_creation_type(
id: &str,
visible: bool,
open: bool,
sort_order: i32,
) -> module_runtime::CreationEntryTypeSnapshot {
module_runtime::CreationEntryTypeSnapshot {
id: id.to_string(),
title: id.to_string(),
subtitle: "测试入口".to_string(),
badge: "测试".to_string(),
image_src: format!("/creation-type-references/{id}.webp"),
visible,
open,
sort_order,
updated_at_micros: 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -172,6 +152,33 @@ mod tests {
resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"),
Some("visual-novel"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
Some("bark-battle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/edutainment/baby-object-match/assets"),
Some("baby-object-match"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/edutainment/baby-love-drawing/magic"),
Some("baby-love-drawing"),
);
assert_eq!(resolve_creation_entry_route_id("/healthz"), None);
}
#[test]
fn test_creation_entry_config_response_keeps_baby_object_match_visible() {
let config = test_creation_entry_config_response();
let baby_object_match = config
.creation_types
.iter()
.find(|item| item.id == "baby-object-match")
.expect("test creation entry config should include baby-object-match");
assert_eq!(baby_object_match.title, "宝贝识物");
assert!(baby_object_match.visible);
assert!(baby_object_match.open);
assert_eq!(baby_object_match.sort_order, 90);
}
}

View File

@@ -0,0 +1,337 @@
use axum::{
Json,
extract::{Extension, State, rejection::JsonRejection},
http::StatusCode,
response::Response,
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::{
api_response::json_success_body,
http_error::AppError,
openai_image_generation::{
DownloadedOpenAiImage, build_openai_image_http_client, create_openai_image_generation,
require_openai_image_settings,
},
request_context::RequestContext,
state::AppState,
};
const BABY_LOVE_DRAWING_PROVIDER: &str = "vector-engine-gpt-image-2";
const BABY_LOVE_DRAWING_IMAGE_SIZE: &str = "1024x1024";
const BABY_LOVE_DRAWING_MAX_STROKES: usize = 600;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CreateBabyLoveDrawingMagicRequest {
original_image_src: String,
#[serde(default)]
stroke_trace: Vec<BabyLoveDrawingStrokePayload>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BabyLoveDrawingStrokePayload {
stroke_id: String,
tool: String,
color: String,
#[serde(default)]
points: Vec<BabyLoveDrawingPointPayload>,
}
#[derive(Debug, Deserialize)]
struct BabyLoveDrawingPointPayload {
x: f64,
y: f64,
t: f64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateBabyLoveDrawingMagicResponse {
magic_image_src: String,
generation_provider: String,
prompt: String,
}
pub async fn create_baby_love_drawing_magic(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
payload: Result<Json<CreateBabyLoveDrawingMagicRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
baby_love_drawing_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "edutainment-baby-drawing",
"message": error.body_text(),
})),
)
})?;
validate_magic_request(&payload)
.map_err(|error| baby_love_drawing_error_response(&request_context, error))?;
let settings = require_openai_image_settings(&state)
.map_err(|error| baby_love_drawing_error_response(&request_context, error))?;
let http_client = build_openai_image_http_client(&settings)
.map_err(|error| baby_love_drawing_error_response(&request_context, error))?;
let prompt = build_baby_love_drawing_magic_prompt(payload.stroke_trace.as_slice());
let reference_images = vec![payload.original_image_src.trim().to_string()];
let generated = create_openai_image_generation(
&http_client,
&settings,
prompt.as_str(),
Some(build_baby_love_drawing_negative_prompt()),
BABY_LOVE_DRAWING_IMAGE_SIZE,
1,
reference_images.as_slice(),
"宝贝爱画绘画魔法图片生成失败",
)
.await
.map_err(|error| baby_love_drawing_error_response(&request_context, error))?;
let generated_image = generated.images.into_iter().next().ok_or_else(|| {
baby_love_drawing_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "宝贝爱画绘画魔法没有返回图片。",
})),
)
})?;
let magic_image_src = build_png_data_url(generated_image)
.map_err(|error| baby_love_drawing_error_response(&request_context, error))?;
Ok(json_success_body(
Some(&request_context),
CreateBabyLoveDrawingMagicResponse {
magic_image_src,
generation_provider: BABY_LOVE_DRAWING_PROVIDER.to_string(),
prompt,
},
))
}
fn validate_magic_request(payload: &CreateBabyLoveDrawingMagicRequest) -> Result<(), AppError> {
let original_image_src = payload.original_image_src.trim();
if !original_image_src.starts_with("data:image/") || !original_image_src.contains(";base64,") {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "edutainment-baby-drawing",
"message": "绘画原图必须是图片 Data URL。",
})),
);
}
if payload.stroke_trace.len() > BABY_LOVE_DRAWING_MAX_STROKES {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "edutainment-baby-drawing",
"message": "绘画笔触数量过多,请重新完成绘画后再使用魔法。",
})),
);
}
Ok(())
}
fn build_baby_love_drawing_magic_prompt(stroke_trace: &[BabyLoveDrawingStrokePayload]) -> String {
let stroke_count = stroke_trace.len();
let brush_count = stroke_trace
.iter()
.filter(|stroke| stroke.tool.trim() == "brush")
.count();
let eraser_count = stroke_trace
.iter()
.filter(|stroke| stroke.tool.trim() == "eraser")
.count();
let color_summary = summarize_stroke_colors(stroke_trace);
let trace_bounds = summarize_trace_bounds(stroke_trace);
format!(
"根据参考图中的儿童绘画内容,为寓教于乐独立关卡“宝贝爱画”生成一张绘本风格图片。\n\
必须保留小朋友原始画面的主体构图、线条方向、颜色关系和童趣笔触,不要改成与原图无关的新内容。\n\
输出风格:明亮、温暖、柔和、卡通绘本风格,适合 4-8 岁儿童,画面干净,边缘柔和,有轻微纸面质感。\n\
笔触信息:总笔触 {stroke_count} 条,画笔 {brush_count} 条,橡皮 {eraser_count} 条,主要颜色 {color_summary},绘制范围 {trace_bounds}\n\
不要生成文字、水印、Logo、按钮、UI 面板、真实照片风、恐怖或成人化内容。"
)
}
fn summarize_stroke_colors(stroke_trace: &[BabyLoveDrawingStrokePayload]) -> String {
let mut colors = Vec::new();
for stroke in stroke_trace {
if stroke.stroke_id.trim().is_empty() {
continue;
}
let color = stroke.color.trim();
if color.is_empty() || colors.iter().any(|value| value == color) {
continue;
}
colors.push(color.to_string());
if colors.len() >= 5 {
break;
}
}
if colors.is_empty() {
"无明显颜色记录".to_string()
} else {
colors.join("")
}
}
fn summarize_trace_bounds(stroke_trace: &[BabyLoveDrawingStrokePayload]) -> String {
let mut min_x = 1.0_f64;
let mut min_y = 1.0_f64;
let mut max_x = 0.0_f64;
let mut max_y = 0.0_f64;
let mut has_point = false;
for point in stroke_trace.iter().flat_map(|stroke| stroke.points.iter()) {
if !(point.x.is_finite() && point.y.is_finite() && point.t.is_finite()) {
continue;
}
has_point = true;
min_x = min_x.min(point.x.clamp(0.0, 1.0));
min_y = min_y.min(point.y.clamp(0.0, 1.0));
max_x = max_x.max(point.x.clamp(0.0, 1.0));
max_y = max_y.max(point.y.clamp(0.0, 1.0));
}
if !has_point {
return "无可用坐标记录".to_string();
}
format!("x {:.2}-{:.2}, y {:.2}-{:.2}", min_x, max_x, min_y, max_y)
}
fn build_baby_love_drawing_negative_prompt() -> &'static str {
"文字水印Logo按钮UI面板复杂背景真实照片风恐怖元素成人化内容攻击性内容替换原图主体完全无关的新画面"
}
fn build_png_data_url(image: DownloadedOpenAiImage) -> Result<String, AppError> {
let png_bytes = normalize_generated_image_to_png(image.bytes.as_slice())?;
Ok(format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(png_bytes)
))
}
fn normalize_generated_image_to_png(source: &[u8]) -> Result<Vec<u8>, AppError> {
let rgba_image = image::load_from_memory(source)
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": format!("解析宝贝爱画魔法图片失败:{error}"),
}))
})?
.to_rgba8();
let (width, height) = rgba_image.dimensions();
let mut encoded = Vec::new();
let encoder = PngEncoder::new(&mut encoded);
encoder
.write_image(rgba_image.as_raw(), width, height, ColorType::Rgba8.into())
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": format!("转换宝贝爱画魔法图片为 PNG 失败:{error}"),
}))
})?;
Ok(encoded)
}
fn baby_love_drawing_error_response(request_context: &RequestContext, error: AppError) -> Response {
error.into_response_with_context(Some(request_context))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_request() -> CreateBabyLoveDrawingMagicRequest {
CreateBabyLoveDrawingMagicRequest {
original_image_src: "data:image/png;base64,abcd".to_string(),
stroke_trace: vec![BabyLoveDrawingStrokePayload {
stroke_id: "stroke-1".to_string(),
tool: "brush".to_string(),
color: "#ef4444".to_string(),
points: vec![
BabyLoveDrawingPointPayload {
x: 0.2,
y: 0.3,
t: 1.0,
},
BabyLoveDrawingPointPayload {
x: 0.7,
y: 0.8,
t: 2.0,
},
],
}],
}
}
#[test]
fn magic_prompt_keeps_child_drawing_and_picture_book_style() {
let request = sample_request();
let prompt = build_baby_love_drawing_magic_prompt(request.stroke_trace.as_slice());
assert!(prompt.contains("宝贝爱画"));
assert!(prompt.contains("绘本风格"));
assert!(prompt.contains("保留小朋友原始画面"));
assert!(prompt.contains("#ef4444"));
assert!(prompt.contains("x 0.20-0.70"));
}
#[test]
fn magic_request_requires_image_data_url() {
let request = sample_request();
assert!(validate_magic_request(&request).is_ok());
let invalid = CreateBabyLoveDrawingMagicRequest {
original_image_src: "https://example.test/image.png".to_string(),
..sample_request()
};
assert!(validate_magic_request(&invalid).is_err());
}
#[test]
fn normalizes_png_to_png_data_url() {
let mut source = Vec::new();
let pixels = vec![255u8; 4 * 2 * 2];
let encoder = PngEncoder::new(&mut source);
encoder
.write_image(pixels.as_slice(), 2, 2, ColorType::Rgba8.into())
.expect("test png should encode");
let image_src = build_png_data_url(DownloadedOpenAiImage {
bytes: source,
mime_type: "image/png".to_string(),
extension: "png".to_string(),
})
.expect("test png should normalize");
assert!(image_src.starts_with("data:image/png;base64,"));
}
#[test]
fn trace_summary_ignores_invalid_points() {
let mut request = sample_request();
request.stroke_trace[0]
.points
.push(BabyLoveDrawingPointPayload {
x: f64::NAN,
y: 0.1,
t: 3.0,
});
assert_eq!(
summarize_trace_bounds(request.stroke_trace.as_slice()),
"x 0.20-0.70, y 0.30-0.80",
);
}
}

View File

@@ -0,0 +1,642 @@
use std::time::Instant;
use axum::{
Json,
extract::{Extension, State, rejection::JsonRejection},
http::StatusCode,
response::Response,
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use futures_util::{StreamExt, stream::FuturesUnordered};
use image::{ColorType, ImageEncoder, codecs::png::PngEncoder};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::{
api_response::json_success_body,
character_visual_assets::try_apply_background_alpha_to_png,
http_error::AppError,
openai_image_generation::{
DownloadedOpenAiImage, OpenAiImageSettings, build_openai_image_http_client,
create_openai_image_generation, require_openai_image_settings,
},
request_context::RequestContext,
state::AppState,
};
const BABY_OBJECT_MATCH_PROVIDER: &str = "vector-engine-gpt-image-2";
const BABY_OBJECT_MATCH_IMAGE_SIZE: &str = "1024x1024";
const BABY_OBJECT_MATCH_BACKGROUND_IMAGE_SIZE: &str = "1536x1024";
const BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS: u64 = 480_000;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GenerateBabyObjectMatchAssetsRequest {
item_names: Vec<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GenerateBabyObjectMatchAssetsResponse {
assets: Vec<BabyObjectMatchItemAssetPayload>,
visual_package: BabyObjectMatchVisualPackagePayload,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BabyObjectMatchItemAssetPayload {
item_id: String,
item_name: String,
image_src: String,
asset_object_id: Option<String>,
generation_provider: String,
prompt: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum BabyObjectMatchVisualAssetKind {
Background,
UiFrame,
GiftBox,
Basket,
SmokePuff,
}
impl BabyObjectMatchVisualAssetKind {
fn asset_id(self) -> &'static str {
match self {
Self::Background => "baby-object-visual-background",
Self::UiFrame => "baby-object-visual-ui-frame",
Self::GiftBox => "baby-object-visual-gift-box",
Self::Basket => "baby-object-visual-basket",
Self::SmokePuff => "baby-object-visual-smoke-puff",
}
}
fn contract_kind(self) -> &'static str {
match self {
Self::Background => "background",
Self::UiFrame => "ui-frame",
Self::GiftBox => "gift-box",
Self::Basket => "basket",
Self::SmokePuff => "smoke-puff",
}
}
fn requires_transparency(self) -> bool {
!matches!(self, Self::Background)
}
fn image_size(self) -> &'static str {
match self {
Self::Background => BABY_OBJECT_MATCH_BACKGROUND_IMAGE_SIZE,
Self::UiFrame | Self::GiftBox | Self::Basket | Self::SmokePuff => {
BABY_OBJECT_MATCH_IMAGE_SIZE
}
}
}
fn failure_context(self) -> &'static str {
match self {
Self::Background => "宝贝识物背景环境图片生成失败",
Self::UiFrame => "宝贝识物 UI 装饰图片生成失败",
Self::GiftBox => "宝贝识物礼物盒图片生成失败",
Self::Basket => "宝贝识物篮子图片生成失败",
Self::SmokePuff => "宝贝识物烟雾特效图片生成失败",
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BabyObjectMatchVisualPackagePayload {
theme_prompt: String,
assets: Vec<BabyObjectMatchVisualAssetPayload>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct BabyObjectMatchVisualAssetPayload {
asset_id: String,
asset_kind: String,
image_src: String,
asset_object_id: Option<String>,
generation_provider: String,
prompt: String,
}
pub async fn generate_baby_object_match_assets(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
payload: Result<Json<GenerateBabyObjectMatchAssetsRequest>, JsonRejection>,
) -> Result<Json<Value>, Response> {
let Json(payload) = payload.map_err(|error| {
baby_object_match_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "edutainment-baby-object",
"message": error.body_text(),
})),
)
})?;
let item_names = normalize_item_names(payload.item_names)
.map_err(|error| baby_object_match_error_response(&request_context, error))?;
let settings = require_openai_image_settings(&state)
.map_err(|error| baby_object_match_error_response(&request_context, error))?;
let settings = with_baby_object_match_image_timeout(settings);
let http_client = build_openai_image_http_client(&settings)
.map_err(|error| baby_object_match_error_response(&request_context, error))?;
let request_started_at = Instant::now();
tracing::info!(
item_count = item_names.len(),
"宝贝识物 image-2 资源生成开始"
);
let (assets, visual_package) = tokio::try_join!(
build_baby_object_match_item_assets(&http_client, &settings, item_names.as_slice()),
build_baby_object_match_visual_package(&http_client, &settings, item_names.as_slice()),
)
.map_err(|error| baby_object_match_error_response(&request_context, error))?;
tracing::info!(
elapsed_ms = request_started_at.elapsed().as_millis() as u64,
"宝贝识物 image-2 资源生成完成"
);
Ok(json_success_body(
Some(&request_context),
GenerateBabyObjectMatchAssetsResponse {
assets,
visual_package,
},
))
}
fn normalize_item_names(item_names: Vec<String>) -> Result<Vec<String>, AppError> {
let normalized = item_names
.into_iter()
.map(|value| value.trim().to_string())
.collect::<Vec<_>>();
if normalized.len() != 2 || normalized.iter().any(|value| value.is_empty()) {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "edutainment-baby-object",
"message": "请填写两个物品名称。",
})),
);
}
Ok(normalized)
}
fn build_baby_object_match_item_prompt(item_name: &str) -> String {
format!(
"为儿童动作 Demo 玩法“宝贝识物”生成物品素材。关键词:{item_name}\n\
风格必须与寓教于乐板块统一:明亮、温暖、卡通绘本质感,适合 4-8 岁儿童,物体边缘清晰,色彩干净,能自然放在草地舞台插画中。\n\
画面只允许出现一个围绕关键词“{item_name}”的单一物品主体,不要生成组合物、多个物体、人物、手、篮子、礼物盒或玩法 UI。\n\
不要生成背景、场景、氛围渲染、阴影地面、文字、水印、边框或按钮。背景必须是纯白或直接透明,便于服务端做透明抠图。\n\
输出为居中完整物品,留少量透明安全边距,最终素材将作为透明 PNG 进入游戏。"
)
}
fn build_baby_object_match_negative_prompt() -> &'static str {
"背景场景草地天空房间光效氛围多个物品组合套装人物篮子礼物盒包装文字标签文字水印LogoUI按钮边框真实照片风复杂投影"
}
fn with_baby_object_match_image_timeout(mut settings: OpenAiImageSettings) -> OpenAiImageSettings {
settings.request_timeout_ms = settings
.request_timeout_ms
.max(BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS);
settings
}
async fn build_baby_object_match_item_assets(
http_client: &reqwest::Client,
settings: &OpenAiImageSettings,
item_names: &[String],
) -> Result<Vec<BabyObjectMatchItemAssetPayload>, AppError> {
let mut pending = FuturesUnordered::new();
// 中文注释:两个物品图互不依赖,并发生成可缩短创作等待时间。
for (index, item_name) in item_names.iter().cloned().enumerate() {
let prompt = build_baby_object_match_item_prompt(item_name.as_str());
pending.push(async move {
let asset_started_at = Instant::now();
tracing::info!(
asset_kind = "item",
item_index = index + 1,
item_name = %item_name,
"宝贝识物 image-2 物品资源生成开始"
);
let generated = create_openai_image_generation(
http_client,
settings,
prompt.as_str(),
Some(build_baby_object_match_negative_prompt()),
BABY_OBJECT_MATCH_IMAGE_SIZE,
1,
&[],
"宝贝识物物品图片生成失败",
)
.await?;
let generated_image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": "宝贝识物物品图片生成没有返回图片。",
}))
})?;
let image_src = build_transparent_png_data_url(generated_image)?;
tracing::info!(
asset_kind = "item",
item_index = index + 1,
item_name = %item_name,
elapsed_ms = asset_started_at.elapsed().as_millis() as u64,
"宝贝识物 image-2 物品资源生成完成"
);
Ok::<_, AppError>(BabyObjectMatchItemAssetPayload {
item_id: format!("baby-object-item-{}", index + 1),
item_name,
image_src,
asset_object_id: None,
generation_provider: BABY_OBJECT_MATCH_PROVIDER.to_string(),
prompt,
})
});
}
let mut assets = Vec::with_capacity(item_names.len());
while let Some(result) = pending.next().await {
assets.push(result?);
}
assets.sort_by_key(|asset| asset.item_id.clone());
Ok(assets)
}
async fn build_baby_object_match_visual_package(
http_client: &reqwest::Client,
settings: &OpenAiImageSettings,
item_names: &[String],
) -> Result<BabyObjectMatchVisualPackagePayload, AppError> {
let package_started_at = Instant::now();
let theme_prompt = build_baby_object_match_visual_theme_prompt(item_names);
let kinds = [
BabyObjectMatchVisualAssetKind::Background,
BabyObjectMatchVisualAssetKind::UiFrame,
BabyObjectMatchVisualAssetKind::GiftBox,
BabyObjectMatchVisualAssetKind::Basket,
BabyObjectMatchVisualAssetKind::SmokePuff,
];
let mut pending = FuturesUnordered::new();
tracing::info!(
asset_count = kinds.len(),
"宝贝识物 image-2 视觉主题包生成开始"
);
for kind in kinds.iter().copied() {
let prompt = build_baby_object_match_visual_asset_prompt(kind, item_names, &theme_prompt);
pending.push(async move {
let asset_started_at = Instant::now();
let asset_kind = kind.contract_kind();
tracing::info!(asset_kind, "宝贝识物 image-2 视觉资源生成开始");
let generated = create_openai_image_generation(
http_client,
settings,
prompt.as_str(),
Some(build_baby_object_match_visual_negative_prompt(kind)),
kind.image_size(),
1,
&[],
kind.failure_context(),
)
.await?;
let generated_image = generated.images.into_iter().next().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": format!("{}VectorEngine 没有返回图片。", kind.failure_context()),
}))
})?;
let image_src = if kind.requires_transparency() {
build_transparent_png_data_url(generated_image)?
} else {
build_png_data_url(generated_image)?
};
tracing::info!(
asset_kind,
elapsed_ms = asset_started_at.elapsed().as_millis() as u64,
"宝贝识物 image-2 视觉资源生成完成"
);
Ok::<_, AppError>(BabyObjectMatchVisualAssetPayload {
asset_id: kind.asset_id().to_string(),
asset_kind: asset_kind.to_string(),
image_src,
asset_object_id: None,
generation_provider: BABY_OBJECT_MATCH_PROVIDER.to_string(),
prompt,
})
});
}
let mut assets = Vec::with_capacity(kinds.len());
while let Some(result) = pending.next().await {
assets.push(result?);
}
assets.sort_by_key(|asset| match asset.asset_kind.as_str() {
"background" => 0,
"ui-frame" => 1,
"gift-box" => 2,
"basket" => 3,
"smoke-puff" => 4,
_ => 5,
});
tracing::info!(
elapsed_ms = package_started_at.elapsed().as_millis() as u64,
"宝贝识物 image-2 视觉主题包生成完成"
);
Ok(BabyObjectMatchVisualPackagePayload {
theme_prompt,
assets,
})
}
fn build_baby_object_match_visual_theme_prompt(item_names: &[String]) -> String {
let item_a = item_names.first().map(String::as_str).unwrap_or_default();
let item_b = item_names.get(1).map(String::as_str).unwrap_or_default();
let theme_hint = resolve_baby_object_match_theme_hint(item_names);
format!(
"根据创作者填写的两个物品关键词“{item_a}”和“{item_b}”,为儿童动作 Demo 玩法“宝贝识物”生成一套完整游戏视觉包装。\n\
视觉必须保持寓教于乐板块统一的明亮、温暖、卡通绘本插画风,适合 4-8 岁儿童。\n\
主题匹配:{theme_hint}\n\
所有资源需要围绕这两个关键词形成统一主题,但不能改变物品识别和左右篮子固定规则。"
)
}
fn resolve_baby_object_match_theme_hint(item_names: &[String]) -> &'static str {
let joined = item_names.join(" ").to_lowercase();
let fruit_keywords = [
"苹果",
"橘子",
"桔子",
"香蕉",
"葡萄",
"草莓",
"西瓜",
"",
"",
"水果",
"apple",
"orange",
"banana",
"grape",
"strawberry",
"watermelon",
"fruit",
];
let character_keywords = [
"佩琪",
"小猪佩奇",
"小猪佩琪",
"奥特曼",
"动漫",
"动画",
"卡通",
"玩具",
"角色",
"公仔",
"peppa",
"ultraman",
"anime",
"cartoon",
"toy",
"doll",
"figure",
];
if fruit_keywords
.iter()
.any(|keyword| joined.contains(keyword))
{
return "若关键词属于水果,背景环境和 UI 元素匹配果园、自然、阳光、树叶等主题。";
}
if character_keywords
.iter()
.any(|keyword| joined.contains(keyword))
{
return "若关键词属于动漫角色、玩具或公仔,背景环境和 UI 元素匹配动漫、玩具房、儿童玩具等主题。";
}
"根据关键词语义自然匹配合适主题,保持儿童寓教于乐插画风。"
}
fn build_baby_object_match_visual_asset_prompt(
kind: BabyObjectMatchVisualAssetKind,
item_names: &[String],
theme_prompt: &str,
) -> String {
let item_a = item_names.first().map(String::as_str).unwrap_or_default();
let item_b = item_names.get(1).map(String::as_str).unwrap_or_default();
let base = format!(
"{theme_prompt}\n\
当前两个关键词:{item_a}{item_b}\n\
输出必须是无文字、无水印、无 Logo 的游戏美术资源。"
);
match kind {
BabyObjectMatchVisualAssetKind::Background => format!(
"{base}\n\
生成游戏背景环境图。背景需要根据关键词主题匹配环境,例如水果可偏果园自然,动漫角色或玩具可偏动漫玩具主题。\n\
保持中间、屏幕中下方和底部左右篮子区域清爽,给放大后的礼物盒、中央物品和左右大篮子预留足够空间,不能画入礼物盒、篮子、物品、人物、文字或操作 UI。"
),
BabyObjectMatchVisualAssetKind::UiFrame => format!(
"{base}\n\
生成透明 PNG 的 UI 装饰框资源,用于字幕条和计数器的风格化包装。\n\
只生成柔和装饰边框、贴纸感边缘和少量主题点缀,不生成任何文字、数字、按钮、图标说明或大面积背景。背景需要纯白或透明友好,便于抠图。"
),
BabyObjectMatchVisualAssetKind::GiftBox => format!(
"{base}\n\
生成透明 PNG 的大号礼物盒资源。礼物盒会在游戏中以约 2 倍视觉尺寸展示,需要主体饱满、轮廓清晰、中心构图、边缘安全留白少,打开动画时可被烟雾遮罩后移除。\n\
礼物盒要与关键词主题匹配,可以带主题贴纸感装饰,但不能出现任何文字、人物、手、篮子或待分类物品。背景需要纯白或透明友好,便于抠图。"
),
BabyObjectMatchVisualAssetKind::Basket => format!(
"{base}\n\
生成透明 PNG 的大号篮子资源,游戏左右两侧会复用同一个篮子造型并以约 1.5 倍视觉尺寸展示。篮子主体要饱满、开口清晰、可读性高、边缘安全留白少。\n\
篮子要与关键词主题匹配,可以有主题色和贴纸感边缘,但不能出现任何文字、礼物盒、人物、手或待分类物品。背景需要纯白或透明友好,便于抠图。"
),
BabyObjectMatchVisualAssetKind::SmokePuff => format!(
"{base}\n\
生成透明 PNG 的烟雾弹出特效资源,用于礼物盒打开瞬间。画面只允许出现一团柔和、圆润、儿童绘本风的云朵烟雾和少量主题色星点,不要生成礼物盒、篮子、物品、人物、手、文字或 UI。\n\
烟雾需要中心构图、边缘柔和、透明边界干净,适合覆盖礼物盒打开区域并衬托中央物品弹出。背景需要纯白或透明友好,便于抠图。"
),
}
}
fn build_baby_object_match_visual_negative_prompt(
kind: BabyObjectMatchVisualAssetKind,
) -> &'static str {
match kind {
BabyObjectMatchVisualAssetKind::Background => {
"文字数字水印Logo按钮说明面板人物礼物盒篮子中心物品复杂前景遮挡真实照片风暗黑风"
}
BabyObjectMatchVisualAssetKind::UiFrame => {
"文字数字水印Logo按钮复杂面板大面积实心背景人物礼物盒篮子物品主体真实照片风"
}
BabyObjectMatchVisualAssetKind::GiftBox => {
"文字数字水印Logo人物篮子待分类物品大面积背景场景真实照片风"
}
BabyObjectMatchVisualAssetKind::Basket => {
"文字数字水印Logo人物礼物盒待分类物品大面积背景场景真实照片风"
}
BabyObjectMatchVisualAssetKind::SmokePuff => {
"文字数字水印Logo人物礼物盒篮子待分类物品大面积背景场景真实照片风硬边爆炸火焰"
}
}
}
fn build_transparent_png_data_url(image: DownloadedOpenAiImage) -> Result<String, AppError> {
let png_bytes = normalize_generated_image_to_png(image.bytes.as_slice())?;
let transparent_png_bytes =
try_apply_background_alpha_to_png(png_bytes.as_slice()).unwrap_or(png_bytes);
Ok(format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(transparent_png_bytes)
))
}
fn build_png_data_url(image: DownloadedOpenAiImage) -> Result<String, AppError> {
let png_bytes = normalize_generated_image_to_png(image.bytes.as_slice())?;
Ok(format!(
"data:image/png;base64,{}",
BASE64_STANDARD.encode(png_bytes)
))
}
fn normalize_generated_image_to_png(source: &[u8]) -> Result<Vec<u8>, AppError> {
let rgba_image = image::load_from_memory(source)
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": format!("解析宝贝识物物品图片失败:{error}"),
}))
})?
.to_rgba8();
let (width, height) = rgba_image.dimensions();
let mut encoded = Vec::new();
let encoder = PngEncoder::new(&mut encoded);
encoder
.write_image(rgba_image.as_raw(), width, height, ColorType::Rgba8.into())
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "vector-engine",
"message": format!("转换宝贝识物物品图片为 PNG 失败:{error}"),
}))
})?;
Ok(encoded)
}
fn baby_object_match_error_response(request_context: &RequestContext, error: AppError) -> Response {
error.into_response_with_context(Some(request_context))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prompt_locks_single_transparent_object_constraints() {
let prompt = build_baby_object_match_item_prompt("苹果");
assert!(prompt.contains("苹果"));
assert!(prompt.contains("卡通绘本"));
assert!(prompt.contains("单一物品"));
assert!(prompt.contains("不要生成背景"));
assert!(prompt.contains("透明 PNG"));
assert!(prompt.contains("纯白或直接透明"));
}
#[test]
fn visual_theme_prompt_maps_fruit_keywords_to_nature_theme() {
let names = vec!["苹果".to_string(), "橘子".to_string()];
let prompt = build_baby_object_match_visual_theme_prompt(names.as_slice());
assert!(prompt.contains("寓教于乐"));
assert!(prompt.contains("卡通绘本"));
assert!(prompt.contains("果园"));
assert!(prompt.contains("自然"));
}
#[test]
fn visual_theme_prompt_maps_character_keywords_to_toy_theme() {
let names = vec!["小猪佩琪".to_string(), "奥特曼".to_string()];
let prompt = build_baby_object_match_visual_theme_prompt(names.as_slice());
assert!(prompt.contains("寓教于乐"));
assert!(prompt.contains("动漫"));
assert!(prompt.contains("玩具"));
}
#[test]
fn visual_asset_prompt_keeps_background_clear_for_playfield() {
let names = vec!["苹果".to_string(), "香蕉".to_string()];
let theme_prompt = build_baby_object_match_visual_theme_prompt(names.as_slice());
let prompt = build_baby_object_match_visual_asset_prompt(
BabyObjectMatchVisualAssetKind::Background,
names.as_slice(),
theme_prompt.as_str(),
);
assert!(prompt.contains("背景环境图"));
assert!(prompt.contains("中间"));
assert!(prompt.contains("屏幕中下方"));
assert!(prompt.contains("无文字"));
}
#[test]
fn normalize_item_names_requires_two_non_empty_names() {
let names = normalize_item_names(vec![" 苹果 ".to_string(), "香蕉".to_string()])
.expect("two names should be valid");
assert_eq!(names, vec!["苹果".to_string(), "香蕉".to_string()]);
assert!(normalize_item_names(vec!["苹果".to_string()]).is_err());
assert!(normalize_item_names(vec!["苹果".to_string(), " ".to_string()]).is_err());
}
#[test]
fn baby_object_match_image_timeout_keeps_long_generation_alive() {
let settings = with_baby_object_match_image_timeout(OpenAiImageSettings {
base_url: "https://vector.example".to_string(),
api_key: "secret".to_string(),
request_timeout_ms: 180_000,
});
assert_eq!(
settings.request_timeout_ms,
BABY_OBJECT_MATCH_VECTOR_ENGINE_REQUEST_TIMEOUT_MS
);
}
#[test]
fn normalizes_png_to_transparent_png_data_url() {
let mut source = Vec::new();
let pixels = vec![255u8; 4 * 2 * 2];
let encoder = PngEncoder::new(&mut source);
encoder
.write_image(pixels.as_slice(), 2, 2, ColorType::Rgba8.into())
.expect("test png should encode");
let image_src = build_transparent_png_data_url(DownloadedOpenAiImage {
bytes: source,
mime_type: "image/png".to_string(),
extension: "png".to_string(),
})
.expect("test png should normalize");
assert!(image_src.starts_with("data:image/png;base64,"));
}
}

View File

@@ -13,6 +13,7 @@ mod auth_payload;
mod auth_public_user;
mod auth_session;
mod auth_sessions;
mod bark_battle;
mod big_fish;
mod big_fish_agent_turn;
mod big_fish_draft_compiler;
@@ -34,6 +35,8 @@ mod custom_world_asset_prompts;
mod custom_world_foundation_draft;
mod custom_world_result_prompts;
mod custom_world_rpg_draft_prompts;
mod edutainment_baby_drawing;
mod edutainment_baby_object;
mod error_middleware;
mod health;
mod http_error;

View File

@@ -0,0 +1,14 @@
[package]
name = "module-bark-battle"
edition.workspace = true
version.workspace = true
license.workspace = true
[features]
default = []
[dependencies]
serde = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }

View File

@@ -0,0 +1,162 @@
use serde::{Deserialize, Serialize};
pub const BARK_BATTLE_RULESET_VERSION_V1: &str = "bark-battle-ruleset-v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DifficultyPreset {
Easy,
Normal,
Hard,
}
impl DifficultyPreset {
pub fn ai_preset_key(self) -> &'static str {
match self {
Self::Easy => "bark-battle-ai-easy",
Self::Normal => "bark-battle-ai-normal",
Self::Hard => "bark-battle-ai-hard",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct RulesetThresholdsSignature {
pub standard_duration_ms: u64,
pub min_duration_ms: u64,
pub max_duration_ms: u64,
pub min_bark_gap_ms: u64,
pub trigger_count_tolerance: u32,
pub min_volume: f32,
pub max_volume: f32,
pub min_average_volume: f32,
pub max_average_volume: f32,
pub min_final_energy: f32,
pub max_final_energy: f32,
pub min_combo: u32,
pub max_combo: u32,
pub draw_threshold_energy: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BarkBattleRuleset {
pub version: &'static str,
pub difficulty: DifficultyPreset,
pub ai_preset_key: &'static str,
pub standard_duration_ms: u64,
pub min_duration_ms: u64,
pub max_duration_ms: u64,
pub min_bark_gap_ms: u64,
pub trigger_count_tolerance: u32,
pub min_volume: f32,
pub max_volume: f32,
pub min_average_volume: f32,
pub max_average_volume: f32,
pub min_final_energy: f32,
pub max_final_energy: f32,
pub min_combo: u32,
pub max_combo: u32,
pub draw_threshold_energy: u32,
}
impl BarkBattleRuleset {
pub fn v1() -> Self {
Self::for_difficulty(DifficultyPreset::Normal)
}
pub fn for_difficulty(difficulty: DifficultyPreset) -> Self {
Self {
version: BARK_BATTLE_RULESET_VERSION_V1,
difficulty,
ai_preset_key: difficulty.ai_preset_key(),
standard_duration_ms: 30_000,
min_duration_ms: 28_000,
max_duration_ms: 35_000,
min_bark_gap_ms: 250,
trigger_count_tolerance: 2,
min_volume: 0.0,
max_volume: 1.0,
min_average_volume: 0.0,
max_average_volume: 1.0,
min_final_energy: 0.0,
max_final_energy: 100.0,
min_combo: 0,
max_combo: 999,
draw_threshold_energy: 3,
}
}
pub fn thresholds_signature(&self) -> RulesetThresholdsSignature {
RulesetThresholdsSignature {
standard_duration_ms: self.standard_duration_ms,
min_duration_ms: self.min_duration_ms,
max_duration_ms: self.max_duration_ms,
min_bark_gap_ms: self.min_bark_gap_ms,
trigger_count_tolerance: self.trigger_count_tolerance,
min_volume: self.min_volume,
max_volume: self.max_volume,
min_average_volume: self.min_average_volume,
max_average_volume: self.max_average_volume,
min_final_energy: self.min_final_energy,
max_final_energy: self.max_final_energy,
min_combo: self.min_combo,
max_combo: self.max_combo,
draw_threshold_energy: self.draw_threshold_energy,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct BarkBattleFinishMetrics {
pub duration_ms: u64,
pub trigger_count: u64,
/// 归一化音量,合法范围为 0.0..=1.0。
pub max_volume: f32,
pub average_volume: f32,
pub final_energy: f32,
pub max_combo: u32,
pub finished_at_micros: i64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinishValidationDecision {
Accepted,
AcceptedWithFlags,
Rejected,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AntiCheatFlag {
DurationTooShort,
DurationTooLong,
TriggerCountTooHigh,
MaxVolumeOutOfRange,
AverageVolumeOutOfRange,
FinalEnergyOutOfRange,
MaxComboOutOfRange,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FinishValidation {
pub decision: FinishValidationDecision,
pub anti_cheat_flags: Vec<AntiCheatFlag>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BattleResult {
PlayerWin,
OpponentWin,
Draw,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BarkBattleLeaderboardScore {
pub final_energy_millis: u32,
pub trigger_count: u64,
pub max_volume_millis: u32,
pub duration_closeness_ms: u64,
pub finished_at_micros: i64,
}

View File

@@ -0,0 +1,5 @@
pub mod domain;
pub mod scoring;
pub use domain::*;
pub use scoring::*;

View File

@@ -0,0 +1,316 @@
use crate::domain::*;
pub fn validate_finish_metrics(
ruleset: &BarkBattleRuleset,
metrics: &BarkBattleFinishMetrics,
) -> FinishValidation {
let mut flags = Vec::new();
let mut rejected = false;
if metrics.duration_ms < ruleset.min_duration_ms {
flags.push(AntiCheatFlag::DurationTooShort);
rejected = true;
}
if metrics.duration_ms > ruleset.max_duration_ms {
flags.push(AntiCheatFlag::DurationTooLong);
rejected = true;
}
let max_trigger_count =
metrics.duration_ms / ruleset.min_bark_gap_ms + u64::from(ruleset.trigger_count_tolerance);
if metrics.trigger_count > max_trigger_count {
flags.push(AntiCheatFlag::TriggerCountTooHigh);
}
if !is_in_range(metrics.max_volume, ruleset.min_volume, ruleset.max_volume) {
flags.push(AntiCheatFlag::MaxVolumeOutOfRange);
rejected = true;
}
if !is_in_range(
metrics.average_volume,
ruleset.min_average_volume,
ruleset.max_average_volume,
) {
flags.push(AntiCheatFlag::AverageVolumeOutOfRange);
rejected = true;
}
if !is_in_range(
metrics.final_energy,
ruleset.min_final_energy,
ruleset.max_final_energy,
) {
flags.push(AntiCheatFlag::FinalEnergyOutOfRange);
rejected = true;
}
if metrics.max_combo < ruleset.min_combo || metrics.max_combo > ruleset.max_combo {
flags.push(AntiCheatFlag::MaxComboOutOfRange);
rejected = true;
}
let decision = if rejected {
FinishValidationDecision::Rejected
} else if flags.is_empty() {
FinishValidationDecision::Accepted
} else {
FinishValidationDecision::AcceptedWithFlags
};
FinishValidation {
decision,
anti_cheat_flags: flags,
}
}
pub fn adjudicate_result(
ruleset: &BarkBattleRuleset,
player_final_energy: f32,
opponent_final_energy: f32,
) -> BattleResult {
let delta = player_final_energy - opponent_final_energy;
if delta.abs() <= ruleset.draw_threshold_energy as f32 {
BattleResult::Draw
} else if delta > 0.0 {
BattleResult::PlayerWin
} else {
BattleResult::OpponentWin
}
}
pub fn compute_leaderboard_score(
ruleset: &BarkBattleRuleset,
metrics: &BarkBattleFinishMetrics,
validation: &FinishValidation,
result: BattleResult,
) -> Option<BarkBattleLeaderboardScore> {
if result != BattleResult::PlayerWin
|| validation.decision == FinishValidationDecision::Rejected
{
return None;
}
Some(BarkBattleLeaderboardScore {
final_energy_millis: to_millis(metrics.final_energy),
trigger_count: metrics.trigger_count,
max_volume_millis: to_millis(metrics.max_volume),
duration_closeness_ms: metrics.duration_ms.abs_diff(ruleset.standard_duration_ms),
finished_at_micros: metrics.finished_at_micros,
})
}
fn is_in_range(value: f32, min: f32, max: f32) -> bool {
value.is_finite() && value >= min && value <= max
}
fn to_millis(value: f32) -> u32 {
(value * 1_000.0).round().clamp(0.0, u32::MAX as f32) as u32
}
#[cfg(test)]
mod tests {
use crate::*;
fn metrics(duration_ms: u64) -> BarkBattleFinishMetrics {
BarkBattleFinishMetrics {
duration_ms,
trigger_count: 10,
max_volume: 0.8,
average_volume: 0.6,
final_energy: 60.0,
max_combo: 5,
finished_at_micros: 1_000_000,
}
}
#[test]
fn serde_uses_contract_snake_case_for_domain_enums() {
assert_eq!(
serde_json::to_value(DifficultyPreset::Easy).expect("serialize difficulty"),
serde_json::json!("easy")
);
assert_eq!(
serde_json::to_value(FinishValidationDecision::AcceptedWithFlags)
.expect("serialize decision"),
serde_json::json!("accepted_with_flags")
);
assert_eq!(
serde_json::to_value(AntiCheatFlag::AverageVolumeOutOfRange)
.expect("serialize anti-cheat flag"),
serde_json::json!("average_volume_out_of_range")
);
assert_eq!(
serde_json::to_value(BattleResult::PlayerWin).expect("serialize battle result"),
serde_json::json!("player_win")
);
}
#[test]
fn accepts_duration_inside_28s_to_35s_window() {
let ruleset = BarkBattleRuleset::v1();
assert_eq!(
validate_finish_metrics(&ruleset, &metrics(28_000)).decision,
FinishValidationDecision::Accepted
);
assert_eq!(
validate_finish_metrics(&ruleset, &metrics(35_000)).decision,
FinishValidationDecision::Accepted
);
}
#[test]
fn rejects_or_flags_extreme_duration() {
let ruleset = BarkBattleRuleset::v1();
assert_ne!(
validate_finish_metrics(&ruleset, &metrics(1_000)).decision,
FinishValidationDecision::Accepted
);
assert_ne!(
validate_finish_metrics(&ruleset, &metrics(300_000)).decision,
FinishValidationDecision::Accepted
);
}
#[test]
fn flags_trigger_count_above_physical_limit_with_tolerance() {
let ruleset = BarkBattleRuleset::v1();
let mut input = metrics(30_000);
input.trigger_count = input.duration_ms / ruleset.min_bark_gap_ms
+ u64::from(ruleset.trigger_count_tolerance)
+ 1;
let validation = validate_finish_metrics(&ruleset, &input);
assert_eq!(
validation.decision,
FinishValidationDecision::AcceptedWithFlags
);
assert!(
validation
.anti_cheat_flags
.contains(&AntiCheatFlag::TriggerCountTooHigh)
);
}
#[test]
fn rejects_final_energy_outside_range() {
let ruleset = BarkBattleRuleset::v1();
let mut input = metrics(30_000);
input.final_energy = ruleset.max_final_energy + 0.1;
let validation = validate_finish_metrics(&ruleset, &input);
assert_eq!(validation.decision, FinishValidationDecision::Rejected);
assert!(
validation
.anti_cheat_flags
.contains(&AntiCheatFlag::FinalEnergyOutOfRange)
);
}
#[test]
fn leaderboard_score_only_for_player_win_and_not_rejected() {
let ruleset = BarkBattleRuleset::v1();
let input = metrics(30_000);
let validation = validate_finish_metrics(&ruleset, &input);
assert!(
compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::PlayerWin)
.is_some()
);
assert!(
compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::Draw).is_none()
);
assert!(
compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::OpponentWin)
.is_none()
);
let rejected = FinishValidation {
decision: FinishValidationDecision::Rejected,
anti_cheat_flags: vec![AntiCheatFlag::DurationTooShort],
};
assert!(
compute_leaderboard_score(&ruleset, &input, &rejected, BattleResult::PlayerWin)
.is_none()
);
}
#[test]
fn adjudicates_draw_threshold_boundaries() {
let ruleset = BarkBattleRuleset::v1();
assert_eq!(adjudicate_result(&ruleset, 53.0, 50.0), BattleResult::Draw);
assert_eq!(
adjudicate_result(&ruleset, 53.1, 50.0),
BattleResult::PlayerWin
);
assert_eq!(
adjudicate_result(&ruleset, 46.9, 50.0),
BattleResult::OpponentWin
);
}
#[test]
fn validates_inclusive_metric_boundaries_and_rejects_non_finite() {
let ruleset = BarkBattleRuleset::v1();
let mut input = metrics(30_000);
input.trigger_count = input.duration_ms / ruleset.min_bark_gap_ms
+ u64::from(ruleset.trigger_count_tolerance);
input.max_volume = ruleset.min_volume;
input.final_energy = ruleset.max_final_energy;
input.max_combo = ruleset.max_combo;
assert_eq!(
validate_finish_metrics(&ruleset, &input).decision,
FinishValidationDecision::Accepted
);
input.max_volume = f32::NAN;
assert_eq!(
validate_finish_metrics(&ruleset, &input).decision,
FinishValidationDecision::Rejected
);
input.max_volume = 0.8;
input.average_volume = ruleset.max_average_volume + 0.1;
let validation = validate_finish_metrics(&ruleset, &input);
assert_eq!(validation.decision, FinishValidationDecision::Rejected);
assert!(
validation
.anti_cheat_flags
.contains(&AntiCheatFlag::AverageVolumeOutOfRange)
);
input.average_volume = 0.6;
input.final_energy = f32::INFINITY;
assert_eq!(
validate_finish_metrics(&ruleset, &input).decision,
FinishValidationDecision::Rejected
);
}
#[test]
fn leaderboard_score_allows_flagged_but_accepted_player_wins() {
let ruleset = BarkBattleRuleset::v1();
let input = metrics(30_000);
let validation = FinishValidation {
decision: FinishValidationDecision::AcceptedWithFlags,
anti_cheat_flags: vec![AntiCheatFlag::TriggerCountTooHigh],
};
assert!(
compute_leaderboard_score(&ruleset, &input, &validation, BattleResult::PlayerWin)
.is_some()
);
}
#[test]
fn difficulty_changes_only_ai_preset_key() {
let easy = BarkBattleRuleset::for_difficulty(DifficultyPreset::Easy);
let normal = BarkBattleRuleset::for_difficulty(DifficultyPreset::Normal);
let hard = BarkBattleRuleset::for_difficulty(DifficultyPreset::Hard);
assert_eq!(easy.thresholds_signature(), normal.thresholds_signature());
assert_eq!(normal.thresholds_signature(), hard.thresholds_signature());
assert_eq!(easy.ai_preset_key, "bark-battle-ai-easy");
assert_eq!(normal.ai_preset_key, "bark-battle-ai-normal");
assert_eq!(hard.ai_preset_key, "bark-battle-ai-hard");
}
}

View File

@@ -46,6 +46,148 @@ pub fn build_creation_entry_config_response(
}
}
pub fn default_creation_entry_type_snapshots(
updated_at_micros: i64,
) -> Vec<CreationEntryTypeSnapshot> {
vec![
build_default_creation_entry_type_snapshot(
"rpg",
"文字冒险",
"经典 RPG 体验",
"内测",
"/creation-type-references/rpg.webp",
false,
true,
10,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"big-fish",
"摸鱼",
"轻量闯关玩法",
"可创建",
"/creation-type-references/big-fish.webp",
false,
true,
20,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"puzzle",
"拼图",
"拼图关卡创作",
"可创建",
"/creation-type-references/puzzle.webp",
true,
true,
30,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"match3d",
"抓大鹅",
"3D 消除关卡",
"可创建",
"/creation-type-references/match3d.webp",
true,
true,
40,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"square-hole",
"方洞",
"形状投放挑战",
"可创建",
"/creation-type-references/square-hole.webp",
false,
true,
50,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"visual-novel",
"视觉小说",
"分支叙事体验",
"敬请期待",
"/creation-type-references/visual-novel.webp",
true,
false,
60,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"airp",
"AI RPG",
"原生角色扮演",
"即将开放",
"/creation-type-references/airp.webp",
true,
false,
70,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"creative-agent",
"智能体创作",
"对话式创作实验",
"内测",
"/creation-type-references/creative-agent.webp",
false,
true,
80,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"bark-battle",
"汪汪声浪",
"声控对战挑战",
"可创建",
"/creation-type-references/creative-agent.webp",
true,
true,
85,
updated_at_micros,
),
build_default_creation_entry_type_snapshot(
"baby-object-match",
"宝贝识物",
"亲子识物分类",
"可创建",
"/child-motion-demo/picture-book-grass-stage.png",
true,
true,
90,
updated_at_micros,
),
]
}
#[allow(clippy::too_many_arguments)]
fn build_default_creation_entry_type_snapshot(
id: &str,
title: &str,
subtitle: &str,
badge: &str,
image_src: &str,
visible: bool,
open: bool,
sort_order: i32,
updated_at_micros: i64,
) -> CreationEntryTypeSnapshot {
CreationEntryTypeSnapshot {
id: id.to_string(),
title: title.to_string(),
subtitle: subtitle.to_string(),
badge: badge.to_string(),
image_src: image_src.to_string(),
visible,
open,
sort_order,
updated_at_micros,
}
}
pub fn build_runtime_setting_record(snapshot: RuntimeSettingSnapshot) -> RuntimeSettingsRecord {
RuntimeSettingsRecord {
user_id: snapshot.user_id,

View File

@@ -209,6 +209,39 @@ mod tests {
assert_eq!(settings.platform_theme, RuntimePlatformTheme::Light);
}
#[test]
fn default_creation_entry_types_include_baby_object_match() {
let configs = default_creation_entry_type_snapshots(1);
let baby_object_match = configs
.iter()
.find(|item| item.id == "baby-object-match")
.expect("baby-object-match creation entry should be seeded");
assert_eq!(baby_object_match.title, "宝贝识物");
assert_eq!(baby_object_match.subtitle, "亲子识物分类");
assert!(baby_object_match.visible);
assert!(baby_object_match.open);
assert_eq!(baby_object_match.sort_order, 90);
assert_eq!(
baby_object_match.image_src,
"/child-motion-demo/picture-book-grass-stage.png"
);
}
#[test]
fn default_creation_entry_types_include_bark_battle() {
let configs = default_creation_entry_type_snapshots(1);
let bark_battle = configs
.iter()
.find(|item| item.id == "bark-battle")
.expect("bark-battle creation entry should be seeded");
assert_eq!(bark_battle.title, "汪汪声浪");
assert!(bark_battle.visible);
assert!(bark_battle.open);
assert_eq!(bark_battle.sort_order, 85);
}
#[test]
fn normalized_clamps_music_volume_into_valid_range() {
let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light);

View File

@@ -0,0 +1,497 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleDifficultyPreset {
Easy,
Normal,
Hard,
}
impl Default for BarkBattleDifficultyPreset {
fn default() -> Self {
Self::Normal
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleServerResult {
PlayerWin,
OpponentWin,
Draw,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleFinishStatus {
Accepted,
AcceptedWithFlags,
Rejected,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleConfigEditorPayload {
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDraftCreateRequest {
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
}
impl From<BarkBattleDraftCreateRequest> for BarkBattleConfigEditorPayload {
fn from(value: BarkBattleDraftCreateRequest) -> Self {
Self {
title: value.title,
description: value.description,
theme_preset: value.theme_preset,
player_dog_skin_preset: value.player_dog_skin_preset,
opponent_dog_skin_preset: value.opponent_dog_skin_preset,
difficulty_preset: value.difficulty_preset,
leaderboard_enabled: value.leaderboard_enabled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleWorkPublishRequest {
pub draft_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_snapshot: Option<BarkBattleConfigEditorPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDraftConfig {
pub draft_id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
pub updated_at: String,
}
impl Default for BarkBattleDraftConfig {
fn default() -> Self {
Self {
draft_id: String::new(),
title: String::new(),
description: None,
theme_preset: String::new(),
player_dog_skin_preset: String::new(),
opponent_dog_skin_preset: String::new(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
leaderboard_enabled: true,
updated_at: String::new(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePublishedConfig {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub draft_id: Option<String>,
pub config_version: u32,
pub ruleset_version: String,
pub play_type_id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
pub updated_at: String,
pub published_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRuntimeConfig {
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub play_type_id: String,
pub duration_ms: u64,
pub energy_min: f32,
pub energy_max: f32,
pub draw_threshold: f32,
pub min_bark_gap_ms: u64,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
pub leaderboard_enabled: bool,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunStartRequest {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_route: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_runtime_version: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunStartResponse {
pub run_id: String,
pub run_token: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub runtime_config: BarkBattleRuntimeConfig,
pub server_started_at: String,
pub expires_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDerivedMetrics {
pub trigger_count: u32,
pub max_volume: f32,
pub average_volume: f32,
pub final_energy: f32,
pub combo_max: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunFinishRequest {
pub work_id: String,
pub run_id: String,
pub run_token: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub client_started_at: String,
pub client_finished_at: String,
pub duration_ms: u64,
pub derived_metrics: BarkBattleDerivedMetrics,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_result: Option<BarkBattleServerResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sample_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_runtime_version: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleScoreSummary {
pub duration_ms: u64,
pub trigger_count: u32,
pub max_volume: f32,
pub average_volume: f32,
pub final_energy: f32,
pub combo_max: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunFinishResponse {
pub status: BarkBattleFinishStatus,
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaderboard_score: Option<u64>,
#[serde(default)]
pub anti_cheat_flags: Vec<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleLeaderboardEntry {
pub rank: u32,
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub display_name: String,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
pub leaderboard_score: u64,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleLeaderboardResponse {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
#[serde(default)]
pub entries: Vec<BarkBattleLeaderboardEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub viewer_best: Option<BarkBattleLeaderboardEntry>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalHistoryItem {
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaderboard_score: Option<u64>,
#[serde(default)]
pub anti_cheat_flags: Vec<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalBestSummary {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_leaderboard_score: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_final_energy: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_trigger_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_max_volume: Option<f32>,
pub win_count: u64,
pub draw_count: u64,
pub loss_count: u64,
pub finish_count: u64,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalHistoryResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub difficulty_preset: Option<BarkBattleDifficultyPreset>,
#[serde(default)]
pub items: Vec<BarkBattlePersonalHistoryItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_summary: Option<BarkBattlePersonalBestSummary>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleWorkStats {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub play_start_count: u64,
pub finish_count: u64,
pub win_count: u64,
pub draw_count: u64,
pub loss_count: u64,
pub flagged_count: u64,
pub leaderboard_entry_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_leaderboard_score: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_final_energy: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub average_final_energy: Option<f32>,
pub updated_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn draft_config_defaults_to_normal_difficulty() {
let config = BarkBattleDraftConfig::default();
assert_eq!(config.difficulty_preset, BarkBattleDifficultyPreset::Normal);
}
#[test]
fn run_requests_carry_config_and_ruleset_identity() {
let start = BarkBattleRunStartRequest {
work_id: "work-1".to_string(),
config_version: Some(3),
source_route: Some("gallery".to_string()),
client_runtime_version: Some("runtime-v1".to_string()),
};
let start_payload = serde_json::to_value(start).expect("start request should serialize");
assert_eq!(start_payload["workId"], json!("work-1"));
assert_eq!(start_payload["configVersion"], json!(3));
assert_eq!(start_payload["sourceRoute"], json!("gallery"));
let finish = BarkBattleRunFinishRequest {
work_id: "work-1".to_string(),
run_id: "run-1".to_string(),
run_token: "token-1".to_string(),
config_version: 3,
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Hard,
client_started_at: "2026-05-13T11:00:00Z".to_string(),
client_finished_at: "2026-05-13T11:00:30Z".to_string(),
duration_ms: 30_000,
derived_metrics: BarkBattleDerivedMetrics {
trigger_count: 12,
max_volume: 0.95,
average_volume: 0.62,
final_energy: 88.5,
combo_max: 7,
},
client_result: Some(BarkBattleServerResult::PlayerWin),
sample_digest: Some("digest-1".to_string()),
client_runtime_version: None,
};
let finish_payload = serde_json::to_value(finish).expect("finish request should serialize");
assert_eq!(finish_payload["configVersion"], json!(3));
assert_eq!(
finish_payload["rulesetVersion"],
json!("bark-battle-ruleset-v1")
);
assert_eq!(finish_payload["difficultyPreset"], json!("hard"));
}
#[test]
fn optional_fields_are_omitted_when_absent() {
let draft = BarkBattleDraftConfig::default();
let payload = serde_json::to_value(draft).expect("draft should serialize");
assert!(!payload.as_object().unwrap().contains_key("description"));
let response = BarkBattlePersonalHistoryResponse {
work_id: None,
difficulty_preset: None,
items: Vec::new(),
best_summary: None,
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
let payload = serde_json::to_value(response).expect("history response should serialize");
assert!(!payload.as_object().unwrap().contains_key("workId"));
assert!(
!payload
.as_object()
.unwrap()
.contains_key("difficultyPreset")
);
assert!(!payload.as_object().unwrap().contains_key("bestSummary"));
}
#[test]
fn finish_response_serializes_player_win_and_accepted() {
let response = BarkBattleRunFinishResponse {
status: BarkBattleFinishStatus::Accepted,
run_id: "run-1".to_string(),
work_id: "work-1".to_string(),
config_version: 3,
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
server_result: BarkBattleServerResult::PlayerWin,
score_summary: BarkBattleScoreSummary {
duration_ms: 30_000,
trigger_count: 12,
max_volume: 0.95,
average_volume: 0.62,
final_energy: 88.5,
combo_max: 7,
},
leaderboard_score: Some(98_765),
anti_cheat_flags: Vec::new(),
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
let payload = serde_json::to_value(response).expect("finish response should serialize");
assert_eq!(payload["runId"], json!("run-1"));
assert_eq!(payload["status"], json!("accepted"));
assert_eq!(payload["serverResult"], json!("player_win"));
assert_eq!(payload["leaderboardScore"], json!(98_765));
assert_eq!(payload["scoreSummary"]["finalEnergy"], json!(88.5));
}
#[test]
fn work_stats_fields_are_constructible() {
let stats = BarkBattleWorkStats {
work_id: "work-1".to_string(),
config_version: Some(3),
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
play_start_count: 10,
finish_count: 9,
win_count: 5,
draw_count: 2,
loss_count: 2,
flagged_count: 1,
leaderboard_entry_count: 4,
best_leaderboard_score: Some(98_765),
best_final_energy: Some(97.5),
average_final_energy: Some(73.25),
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
assert_eq!(stats.work_id, "work-1");
assert_eq!(stats.play_start_count, 10);
assert_eq!(stats.finish_count, 9);
assert_eq!(stats.win_count, 5);
assert_eq!(stats.draw_count, 2);
assert_eq!(stats.loss_count, 2);
assert_eq!(stats.flagged_count, 1);
assert_eq!(stats.leaderboard_entry_count, 4);
assert_eq!(stats.best_leaderboard_score, Some(98_765));
assert_eq!(stats.best_final_energy, Some(97.5));
assert_eq!(stats.average_final_energy, Some(73.25));
assert_eq!(stats.updated_at, "2026-05-13T11:00:00Z");
}
}

View File

@@ -4,6 +4,7 @@ pub mod api;
#[cfg(feature = "oss-contracts")]
pub mod assets;
pub mod auth;
pub mod bark_battle;
pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;

View File

@@ -0,0 +1,138 @@
use super::*;
pub type BarkBattleDraftCreateRecordInput = BarkBattleDraftCreateInput;
pub type BarkBattleDraftConfigUpsertRecordInput = BarkBattleDraftConfigUpsertInput;
pub type BarkBattleWorkPublishRecordInput = BarkBattleWorkPublishInput;
pub type BarkBattleRunStartRecordInput = BarkBattleRunStartInput;
pub type BarkBattleRunFinishRecordInput = BarkBattleRunFinishInput;
impl SpacetimeClient {
pub async fn create_bark_battle_draft(
&self,
input: BarkBattleDraftCreateRecordInput,
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.create_bark_battle_draft_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_draft_config_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn update_bark_battle_draft_config(
&self,
input: BarkBattleDraftConfigUpsertRecordInput,
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.update_bark_battle_draft_config_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_draft_config_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn publish_bark_battle_work(
&self,
input: BarkBattleWorkPublishRecordInput,
) -> Result<BarkBattleRuntimeConfigRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.publish_bark_battle_work_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_runtime_config_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn get_bark_battle_runtime_config(
&self,
work_id: String,
owner_user_id: Option<String>,
) -> Result<BarkBattleRuntimeConfigRecord, SpacetimeClientError> {
let input = BarkBattleRuntimeConfigGetInput {
work_id,
owner_user_id,
};
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.get_bark_battle_runtime_config_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_runtime_config_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn start_bark_battle_run(
&self,
input: BarkBattleRunStartRecordInput,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.start_bark_battle_run_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_run_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn finish_bark_battle_run(
&self,
input: BarkBattleRunFinishRecordInput,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.finish_bark_battle_run_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_run_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
pub async fn get_bark_battle_run(
&self,
run_id: String,
owner_user_id: String,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
let input = BarkBattleRunGetInput {
run_id,
owner_user_id,
};
self.call_after_connect(move |connection, sender| {
connection
.procedures()
.get_bark_battle_run_then(input, move |_, result| {
let mapped = result
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
.and_then(map_bark_battle_run_procedure_result);
send_once(&sender, mapped);
});
})
.await
}
}

View File

@@ -6,11 +6,12 @@ mod mapper;
use mapper::*;
pub use mapper::{
AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord,
AiTextChunkRecord, BattleStateRecord, BigFishAgentMessageRecord, BigFishAnchorItemRecord,
BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput,
BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishDraftCompileRecordInput,
BigFishGameDraftRecord, BigFishInputSubmitRecordInput, BigFishLevelBlueprintRecord,
BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput,
AiTextChunkRecord, BarkBattleDraftConfigRecord, BarkBattleRunRecord,
BarkBattleRuntimeConfigRecord, BattleStateRecord, BigFishAgentMessageRecord,
BigFishAnchorItemRecord, BigFishAnchorPackRecord, BigFishAssetCoverageRecord,
BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord,
BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput,
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput,
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord,
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
@@ -74,6 +75,12 @@ pub use mapper::{
pub mod ai;
pub mod assets;
pub mod auth;
pub mod bark_battle;
pub use bark_battle::{
BarkBattleDraftConfigUpsertRecordInput, BarkBattleDraftCreateRecordInput,
BarkBattleRunFinishRecordInput, BarkBattleRunStartRecordInput,
BarkBattleWorkPublishRecordInput,
};
pub mod big_fish;
pub mod combat;
pub mod custom_world;

View File

@@ -732,6 +732,42 @@ pub(crate) fn map_asset_history_list_result(
.collect())
}
pub type BarkBattleDraftConfigRecord = serde_json::Value;
pub type BarkBattleRuntimeConfigRecord = serde_json::Value;
pub type BarkBattleRunRecord = serde_json::Value;
pub(crate) fn map_bark_battle_draft_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleDraftConfigRecord, SpacetimeClientError> {
parse_bark_battle_row_json(result, "Bark Battle draft config")
}
pub(crate) fn map_bark_battle_runtime_config_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRuntimeConfigRecord, SpacetimeClientError> {
parse_bark_battle_row_json(result, "Bark Battle runtime config")
}
pub(crate) fn map_bark_battle_run_procedure_result(
result: BarkBattleProcedureResult,
) -> Result<BarkBattleRunRecord, SpacetimeClientError> {
parse_bark_battle_row_json(result, "Bark Battle run")
}
fn parse_bark_battle_row_json<T: serde::de::DeserializeOwned>(
result: BarkBattleProcedureResult,
label: &'static str,
) -> Result<T, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let row_json = result
.row_json
.ok_or_else(|| SpacetimeClientError::missing_snapshot(label))?;
serde_json::from_str(&row_json)
.map_err(|error| SpacetimeClientError::Runtime(format!("{label} JSON 解析失败: {error}")))
}
pub type CreationEntryConfigRecord =
shared_contracts::creation_entry_config::CreationEntryConfigResponse;

View File

@@ -0,0 +1,86 @@
// 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 BarkBattleDraftConfigRow {
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: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattleDraftConfigRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattleDraftConfigRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattleDraftConfigRowCols {
pub draft_id: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub work_id: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub config_version: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, u64>,
pub ruleset_version: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub difficulty_preset: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub leaderboard_enabled: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, bool>,
pub config_json: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub editor_state_json: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, String>,
pub created_at: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<BarkBattleDraftConfigRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattleDraftConfigRow {
type Cols = BarkBattleDraftConfigRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattleDraftConfigRowCols {
draft_id: __sdk::__query_builder::Col::new(table_name, "draft_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
config_version: __sdk::__query_builder::Col::new(table_name, "config_version"),
ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"),
difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"),
leaderboard_enabled: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_enabled",
),
config_json: __sdk::__query_builder::Col::new(table_name, "config_json"),
editor_state_json: __sdk::__query_builder::Col::new(table_name, "editor_state_json"),
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 `BarkBattleDraftConfigRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattleDraftConfigRowIxCols {
pub draft_id: __sdk::__query_builder::IxCol<BarkBattleDraftConfigRow, String>,
pub owner_user_id: __sdk::__query_builder::IxCol<BarkBattleDraftConfigRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattleDraftConfigRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattleDraftConfigRow {
type IxCols = BarkBattleDraftConfigRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattleDraftConfigRowIxCols {
draft_id: __sdk::__query_builder::IxCol::new(table_name, "draft_id"),
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattleDraftConfigRow {}

View File

@@ -0,0 +1,162 @@
// 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 super::bark_battle_draft_config_row_type::BarkBattleDraftConfigRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_draft_config`.
///
/// Obtain a handle from the [`BarkBattleDraftConfigTableAccess::bark_battle_draft_config`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_draft_config()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_draft_config().on_insert(...)`.
pub struct BarkBattleDraftConfigTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattleDraftConfigRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_draft_config`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattleDraftConfigTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattleDraftConfigTableHandle`], which mediates access to the table `bark_battle_draft_config`.
fn bark_battle_draft_config(&self) -> BarkBattleDraftConfigTableHandle<'_>;
}
impl BarkBattleDraftConfigTableAccess for super::RemoteTables {
fn bark_battle_draft_config(&self) -> BarkBattleDraftConfigTableHandle<'_> {
BarkBattleDraftConfigTableHandle {
imp: self
.imp
.get_table::<BarkBattleDraftConfigRow>("bark_battle_draft_config"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattleDraftConfigInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattleDraftConfigDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattleDraftConfigTableHandle<'ctx> {
type Row = BarkBattleDraftConfigRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattleDraftConfigRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattleDraftConfigInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleDraftConfigInsertCallbackId {
BarkBattleDraftConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattleDraftConfigInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattleDraftConfigDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleDraftConfigDeleteCallbackId {
BarkBattleDraftConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattleDraftConfigDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattleDraftConfigUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleDraftConfigTableHandle<'ctx> {
type UpdateCallbackId = BarkBattleDraftConfigUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattleDraftConfigUpdateCallbackId {
BarkBattleDraftConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattleDraftConfigUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `draft_id` unique index on the table `bark_battle_draft_config`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattleDraftConfigDraftIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_draft_config().draft_id().find(...)`.
pub struct BarkBattleDraftConfigDraftIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattleDraftConfigRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattleDraftConfigTableHandle<'ctx> {
/// Get a handle on the `draft_id` unique index on the table `bark_battle_draft_config`.
pub fn draft_id(&self) -> BarkBattleDraftConfigDraftIdUnique<'ctx> {
BarkBattleDraftConfigDraftIdUnique {
imp: self.imp.get_unique_constraint::<String>("draft_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattleDraftConfigDraftIdUnique<'ctx> {
/// Find the subscribed row whose `draft_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<BarkBattleDraftConfigRow> {
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::<BarkBattleDraftConfigRow>("bark_battle_draft_config");
_table.add_unique_constraint::<String>("draft_id", |row| &row.draft_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleDraftConfigRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<BarkBattleDraftConfigRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattleDraftConfigRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_draft_configQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattleDraftConfigRow`.
fn bark_battle_draft_config(&self) -> __sdk::__query_builder::Table<BarkBattleDraftConfigRow>;
}
impl bark_battle_draft_configQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_draft_config(&self) -> __sdk::__query_builder::Table<BarkBattleDraftConfigRow> {
__sdk::__query_builder::Table::new("bark_battle_draft_config")
}
}

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 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,
}
impl __sdk::InModule for BarkBattleDraftConfigUpsertInput {
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};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
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,
}
impl __sdk::InModule for BarkBattleDraftCreateInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,94 @@
// 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 BarkBattleLeaderboardEntryRow {
pub leaderboard_entry_id: String,
pub work_id: String,
pub owner_user_id: String,
pub run_id: String,
pub score_id: String,
pub leaderboard_score: u64,
pub final_energy: f32,
pub trigger_count: u64,
pub max_volume: f32,
pub duration_closeness_ms: u64,
pub finished_at_micros: i64,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattleLeaderboardEntryRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattleLeaderboardEntryRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattleLeaderboardEntryRowCols {
pub leaderboard_entry_id: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, String>,
pub work_id: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, String>,
pub run_id: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, String>,
pub score_id: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, String>,
pub leaderboard_score: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, u64>,
pub final_energy: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, f32>,
pub trigger_count: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, u64>,
pub max_volume: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, f32>,
pub duration_closeness_ms: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, u64>,
pub finished_at_micros: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, i64>,
pub created_at: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<BarkBattleLeaderboardEntryRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattleLeaderboardEntryRow {
type Cols = BarkBattleLeaderboardEntryRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattleLeaderboardEntryRowCols {
leaderboard_entry_id: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_entry_id",
),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
score_id: __sdk::__query_builder::Col::new(table_name, "score_id"),
leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"),
final_energy: __sdk::__query_builder::Col::new(table_name, "final_energy"),
trigger_count: __sdk::__query_builder::Col::new(table_name, "trigger_count"),
max_volume: __sdk::__query_builder::Col::new(table_name, "max_volume"),
duration_closeness_ms: __sdk::__query_builder::Col::new(
table_name,
"duration_closeness_ms",
),
finished_at_micros: __sdk::__query_builder::Col::new(table_name, "finished_at_micros"),
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 `BarkBattleLeaderboardEntryRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattleLeaderboardEntryRowIxCols {
pub leaderboard_entry_id: __sdk::__query_builder::IxCol<BarkBattleLeaderboardEntryRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattleLeaderboardEntryRow {
type IxCols = BarkBattleLeaderboardEntryRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattleLeaderboardEntryRowIxCols {
leaderboard_entry_id: __sdk::__query_builder::IxCol::new(
table_name,
"leaderboard_entry_id",
),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattleLeaderboardEntryRow {}

View File

@@ -0,0 +1,171 @@
// 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 super::bark_battle_leaderboard_entry_row_type::BarkBattleLeaderboardEntryRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_leaderboard_entry`.
///
/// Obtain a handle from the [`BarkBattleLeaderboardEntryTableAccess::bark_battle_leaderboard_entry`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_leaderboard_entry()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_leaderboard_entry().on_insert(...)`.
pub struct BarkBattleLeaderboardEntryTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattleLeaderboardEntryRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_leaderboard_entry`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattleLeaderboardEntryTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattleLeaderboardEntryTableHandle`], which mediates access to the table `bark_battle_leaderboard_entry`.
fn bark_battle_leaderboard_entry(&self) -> BarkBattleLeaderboardEntryTableHandle<'_>;
}
impl BarkBattleLeaderboardEntryTableAccess for super::RemoteTables {
fn bark_battle_leaderboard_entry(&self) -> BarkBattleLeaderboardEntryTableHandle<'_> {
BarkBattleLeaderboardEntryTableHandle {
imp: self
.imp
.get_table::<BarkBattleLeaderboardEntryRow>("bark_battle_leaderboard_entry"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattleLeaderboardEntryInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattleLeaderboardEntryDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattleLeaderboardEntryTableHandle<'ctx> {
type Row = BarkBattleLeaderboardEntryRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattleLeaderboardEntryRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattleLeaderboardEntryInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleLeaderboardEntryInsertCallbackId {
BarkBattleLeaderboardEntryInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattleLeaderboardEntryInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattleLeaderboardEntryDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleLeaderboardEntryDeleteCallbackId {
BarkBattleLeaderboardEntryDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattleLeaderboardEntryDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattleLeaderboardEntryUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleLeaderboardEntryTableHandle<'ctx> {
type UpdateCallbackId = BarkBattleLeaderboardEntryUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattleLeaderboardEntryUpdateCallbackId {
BarkBattleLeaderboardEntryUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattleLeaderboardEntryUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `leaderboard_entry_id` unique index on the table `bark_battle_leaderboard_entry`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattleLeaderboardEntryLeaderboardEntryIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_leaderboard_entry().leaderboard_entry_id().find(...)`.
pub struct BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattleLeaderboardEntryRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattleLeaderboardEntryTableHandle<'ctx> {
/// Get a handle on the `leaderboard_entry_id` unique index on the table `bark_battle_leaderboard_entry`.
pub fn leaderboard_entry_id(&self) -> BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> {
BarkBattleLeaderboardEntryLeaderboardEntryIdUnique {
imp: self
.imp
.get_unique_constraint::<String>("leaderboard_entry_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattleLeaderboardEntryLeaderboardEntryIdUnique<'ctx> {
/// Find the subscribed row whose `leaderboard_entry_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<BarkBattleLeaderboardEntryRow> {
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::<BarkBattleLeaderboardEntryRow>("bark_battle_leaderboard_entry");
_table.add_unique_constraint::<String>("leaderboard_entry_id", |row| &row.leaderboard_entry_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleLeaderboardEntryRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<BarkBattleLeaderboardEntryRow>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattleLeaderboardEntryRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_leaderboard_entryQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattleLeaderboardEntryRow`.
fn bark_battle_leaderboard_entry(
&self,
) -> __sdk::__query_builder::Table<BarkBattleLeaderboardEntryRow>;
}
impl bark_battle_leaderboard_entryQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_leaderboard_entry(
&self,
) -> __sdk::__query_builder::Table<BarkBattleLeaderboardEntryRow> {
__sdk::__query_builder::Table::new("bark_battle_leaderboard_entry")
}
}

View File

@@ -0,0 +1,107 @@
// 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 BarkBattlePersonalBestProjectionRow {
pub personal_best_id: String,
pub owner_user_id: String,
pub work_id: String,
pub run_id: String,
pub score_id: String,
pub leaderboard_entry_id: Option<String>,
pub leaderboard_score: Option<u64>,
pub final_energy: f32,
pub trigger_count: u64,
pub max_volume: f32,
pub duration_closeness_ms: u64,
pub server_result: String,
pub validation_status: String,
pub finished_at_micros: i64,
pub summary_json: String,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattlePersonalBestProjectionRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattlePersonalBestProjectionRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattlePersonalBestProjectionRowCols {
pub personal_best_id: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub work_id: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub run_id: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub score_id: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub leaderboard_entry_id:
__sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, Option<String>>,
pub leaderboard_score:
__sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, Option<u64>>,
pub final_energy: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, f32>,
pub trigger_count: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, u64>,
pub max_volume: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, f32>,
pub duration_closeness_ms:
__sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, u64>,
pub server_result: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub validation_status: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub finished_at_micros: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, i64>,
pub summary_json: __sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, String>,
pub updated_at:
__sdk::__query_builder::Col<BarkBattlePersonalBestProjectionRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattlePersonalBestProjectionRow {
type Cols = BarkBattlePersonalBestProjectionRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattlePersonalBestProjectionRowCols {
personal_best_id: __sdk::__query_builder::Col::new(table_name, "personal_best_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
score_id: __sdk::__query_builder::Col::new(table_name, "score_id"),
leaderboard_entry_id: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_entry_id",
),
leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"),
final_energy: __sdk::__query_builder::Col::new(table_name, "final_energy"),
trigger_count: __sdk::__query_builder::Col::new(table_name, "trigger_count"),
max_volume: __sdk::__query_builder::Col::new(table_name, "max_volume"),
duration_closeness_ms: __sdk::__query_builder::Col::new(
table_name,
"duration_closeness_ms",
),
server_result: __sdk::__query_builder::Col::new(table_name, "server_result"),
validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"),
finished_at_micros: __sdk::__query_builder::Col::new(table_name, "finished_at_micros"),
summary_json: __sdk::__query_builder::Col::new(table_name, "summary_json"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `BarkBattlePersonalBestProjectionRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattlePersonalBestProjectionRowIxCols {
pub personal_best_id:
__sdk::__query_builder::IxCol<BarkBattlePersonalBestProjectionRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattlePersonalBestProjectionRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattlePersonalBestProjectionRow {
type IxCols = BarkBattlePersonalBestProjectionRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattlePersonalBestProjectionRowIxCols {
personal_best_id: __sdk::__query_builder::IxCol::new(table_name, "personal_best_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattlePersonalBestProjectionRow {}

View File

@@ -0,0 +1,174 @@
// 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 super::bark_battle_personal_best_projection_row_type::BarkBattlePersonalBestProjectionRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_personal_best_projection`.
///
/// Obtain a handle from the [`BarkBattlePersonalBestProjectionTableAccess::bark_battle_personal_best_projection`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_personal_best_projection()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_personal_best_projection().on_insert(...)`.
pub struct BarkBattlePersonalBestProjectionTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattlePersonalBestProjectionRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_personal_best_projection`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattlePersonalBestProjectionTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattlePersonalBestProjectionTableHandle`], which mediates access to the table `bark_battle_personal_best_projection`.
fn bark_battle_personal_best_projection(
&self,
) -> BarkBattlePersonalBestProjectionTableHandle<'_>;
}
impl BarkBattlePersonalBestProjectionTableAccess for super::RemoteTables {
fn bark_battle_personal_best_projection(
&self,
) -> BarkBattlePersonalBestProjectionTableHandle<'_> {
BarkBattlePersonalBestProjectionTableHandle {
imp: self.imp.get_table::<BarkBattlePersonalBestProjectionRow>(
"bark_battle_personal_best_projection",
),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattlePersonalBestProjectionInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattlePersonalBestProjectionDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattlePersonalBestProjectionTableHandle<'ctx> {
type Row = BarkBattlePersonalBestProjectionRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattlePersonalBestProjectionRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattlePersonalBestProjectionInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattlePersonalBestProjectionInsertCallbackId {
BarkBattlePersonalBestProjectionInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattlePersonalBestProjectionInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattlePersonalBestProjectionDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattlePersonalBestProjectionDeleteCallbackId {
BarkBattlePersonalBestProjectionDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattlePersonalBestProjectionDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattlePersonalBestProjectionUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattlePersonalBestProjectionTableHandle<'ctx> {
type UpdateCallbackId = BarkBattlePersonalBestProjectionUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattlePersonalBestProjectionUpdateCallbackId {
BarkBattlePersonalBestProjectionUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattlePersonalBestProjectionUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `personal_best_id` unique index on the table `bark_battle_personal_best_projection`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattlePersonalBestProjectionPersonalBestIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_personal_best_projection().personal_best_id().find(...)`.
pub struct BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattlePersonalBestProjectionRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattlePersonalBestProjectionTableHandle<'ctx> {
/// Get a handle on the `personal_best_id` unique index on the table `bark_battle_personal_best_projection`.
pub fn personal_best_id(&self) -> BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> {
BarkBattlePersonalBestProjectionPersonalBestIdUnique {
imp: self.imp.get_unique_constraint::<String>("personal_best_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattlePersonalBestProjectionPersonalBestIdUnique<'ctx> {
/// Find the subscribed row whose `personal_best_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<BarkBattlePersonalBestProjectionRow> {
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::<BarkBattlePersonalBestProjectionRow>(
"bark_battle_personal_best_projection",
);
_table.add_unique_constraint::<String>("personal_best_id", |row| &row.personal_best_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattlePersonalBestProjectionRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<BarkBattlePersonalBestProjectionRow>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattlePersonalBestProjectionRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_personal_best_projectionQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattlePersonalBestProjectionRow`.
fn bark_battle_personal_best_projection(
&self,
) -> __sdk::__query_builder::Table<BarkBattlePersonalBestProjectionRow>;
}
impl bark_battle_personal_best_projectionQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_personal_best_projection(
&self,
) -> __sdk::__query_builder::Table<BarkBattlePersonalBestProjectionRow> {
__sdk::__query_builder::Table::new("bark_battle_personal_best_projection")
}
}

View File

@@ -0,0 +1,17 @@
// 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 BarkBattleProcedureResult {
pub ok: bool,
pub row_json: Option<String>,
pub error_message: Option<String>,
}
impl __sdk::InModule for BarkBattleProcedureResult {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,90 @@
// 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 BarkBattlePublishedConfigRow {
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 created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
pub published_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattlePublishedConfigRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattlePublishedConfigRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattlePublishedConfigRowCols {
pub work_id: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub source_draft_id: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, Option<String>>,
pub config_version: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, u64>,
pub ruleset_version: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub difficulty_preset: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub leaderboard_enabled: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, bool>,
pub config_json: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub published_snapshot_json: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, String>,
pub created_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
pub published_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattlePublishedConfigRow {
type Cols = BarkBattlePublishedConfigRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattlePublishedConfigRowCols {
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
source_draft_id: __sdk::__query_builder::Col::new(table_name, "source_draft_id"),
config_version: __sdk::__query_builder::Col::new(table_name, "config_version"),
ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"),
difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"),
leaderboard_enabled: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_enabled",
),
config_json: __sdk::__query_builder::Col::new(table_name, "config_json"),
published_snapshot_json: __sdk::__query_builder::Col::new(
table_name,
"published_snapshot_json",
),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
}
}
}
/// Indexed column accessor struct for the table `BarkBattlePublishedConfigRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattlePublishedConfigRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<BarkBattlePublishedConfigRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattlePublishedConfigRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattlePublishedConfigRow {
type IxCols = BarkBattlePublishedConfigRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattlePublishedConfigRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattlePublishedConfigRow {}

View File

@@ -0,0 +1,169 @@
// 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 super::bark_battle_published_config_row_type::BarkBattlePublishedConfigRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_published_config`.
///
/// Obtain a handle from the [`BarkBattlePublishedConfigTableAccess::bark_battle_published_config`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_published_config()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_published_config().on_insert(...)`.
pub struct BarkBattlePublishedConfigTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattlePublishedConfigRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_published_config`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattlePublishedConfigTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattlePublishedConfigTableHandle`], which mediates access to the table `bark_battle_published_config`.
fn bark_battle_published_config(&self) -> BarkBattlePublishedConfigTableHandle<'_>;
}
impl BarkBattlePublishedConfigTableAccess for super::RemoteTables {
fn bark_battle_published_config(&self) -> BarkBattlePublishedConfigTableHandle<'_> {
BarkBattlePublishedConfigTableHandle {
imp: self
.imp
.get_table::<BarkBattlePublishedConfigRow>("bark_battle_published_config"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattlePublishedConfigInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattlePublishedConfigDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattlePublishedConfigTableHandle<'ctx> {
type Row = BarkBattlePublishedConfigRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattlePublishedConfigRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattlePublishedConfigInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattlePublishedConfigInsertCallbackId {
BarkBattlePublishedConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattlePublishedConfigInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattlePublishedConfigDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattlePublishedConfigDeleteCallbackId {
BarkBattlePublishedConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattlePublishedConfigDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattlePublishedConfigUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattlePublishedConfigTableHandle<'ctx> {
type UpdateCallbackId = BarkBattlePublishedConfigUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattlePublishedConfigUpdateCallbackId {
BarkBattlePublishedConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattlePublishedConfigUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `work_id` unique index on the table `bark_battle_published_config`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattlePublishedConfigWorkIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_published_config().work_id().find(...)`.
pub struct BarkBattlePublishedConfigWorkIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattlePublishedConfigRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattlePublishedConfigTableHandle<'ctx> {
/// Get a handle on the `work_id` unique index on the table `bark_battle_published_config`.
pub fn work_id(&self) -> BarkBattlePublishedConfigWorkIdUnique<'ctx> {
BarkBattlePublishedConfigWorkIdUnique {
imp: self.imp.get_unique_constraint::<String>("work_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattlePublishedConfigWorkIdUnique<'ctx> {
/// Find the subscribed row whose `work_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<BarkBattlePublishedConfigRow> {
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::<BarkBattlePublishedConfigRow>("bark_battle_published_config");
_table.add_unique_constraint::<String>("work_id", |row| &row.work_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattlePublishedConfigRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<BarkBattlePublishedConfigRow>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattlePublishedConfigRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_published_configQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattlePublishedConfigRow`.
fn bark_battle_published_config(
&self,
) -> __sdk::__query_builder::Table<BarkBattlePublishedConfigRow>;
}
impl bark_battle_published_configQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_published_config(
&self,
) -> __sdk::__query_builder::Table<BarkBattlePublishedConfigRow> {
__sdk::__query_builder::Table::new("bark_battle_published_config")
}
}

View File

@@ -0,0 +1,32 @@
// 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 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,
}
impl __sdk::InModule for BarkBattleRunFinishInput {
type Module = super::RemoteModule;
}

View File

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

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 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,
}
impl __sdk::InModule for BarkBattleRunStartInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,16 @@
// 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 BarkBattleRuntimeConfigGetInput {
pub work_id: String,
pub owner_user_id: Option<String>,
}
impl __sdk::InModule for BarkBattleRuntimeConfigGetInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,127 @@
// 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 BarkBattleRuntimeRunRow {
pub run_id: String,
pub run_token_hash: 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: __sdk::Timestamp,
pub client_finished_at_micros: Option<i64>,
pub server_finished_at: Option<__sdk::Timestamp>,
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>,
pub created_at: __sdk::Timestamp,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattleRuntimeRunRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattleRuntimeRunRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattleRuntimeRunRowCols {
pub run_id: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub run_token_hash: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub work_id: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub config_version: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, u64>,
pub ruleset_version: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub difficulty_preset: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub leaderboard_enabled: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, bool>,
pub status: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub client_started_at_micros: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, i64>,
pub server_started_at: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, __sdk::Timestamp>,
pub client_finished_at_micros:
__sdk::__query_builder::Col<BarkBattleRuntimeRunRow, Option<i64>>,
pub server_finished_at:
__sdk::__query_builder::Col<BarkBattleRuntimeRunRow, Option<__sdk::Timestamp>>,
pub metrics_json: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub server_result: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, Option<String>>,
pub validation_status: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub anti_cheat_flags_json: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, String>,
pub leaderboard_score: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, Option<u64>>,
pub score_id: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, Option<String>>,
pub created_at: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, __sdk::Timestamp>,
pub updated_at: __sdk::__query_builder::Col<BarkBattleRuntimeRunRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattleRuntimeRunRow {
type Cols = BarkBattleRuntimeRunRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattleRuntimeRunRowCols {
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
run_token_hash: __sdk::__query_builder::Col::new(table_name, "run_token_hash"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
config_version: __sdk::__query_builder::Col::new(table_name, "config_version"),
ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"),
difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"),
leaderboard_enabled: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_enabled",
),
status: __sdk::__query_builder::Col::new(table_name, "status"),
client_started_at_micros: __sdk::__query_builder::Col::new(
table_name,
"client_started_at_micros",
),
server_started_at: __sdk::__query_builder::Col::new(table_name, "server_started_at"),
client_finished_at_micros: __sdk::__query_builder::Col::new(
table_name,
"client_finished_at_micros",
),
server_finished_at: __sdk::__query_builder::Col::new(table_name, "server_finished_at"),
metrics_json: __sdk::__query_builder::Col::new(table_name, "metrics_json"),
server_result: __sdk::__query_builder::Col::new(table_name, "server_result"),
validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"),
anti_cheat_flags_json: __sdk::__query_builder::Col::new(
table_name,
"anti_cheat_flags_json",
),
leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"),
score_id: __sdk::__query_builder::Col::new(table_name, "score_id"),
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 `BarkBattleRuntimeRunRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattleRuntimeRunRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<BarkBattleRuntimeRunRow, String>,
pub run_id: __sdk::__query_builder::IxCol<BarkBattleRuntimeRunRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattleRuntimeRunRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattleRuntimeRunRow {
type IxCols = BarkBattleRuntimeRunRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattleRuntimeRunRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattleRuntimeRunRow {}

View File

@@ -0,0 +1,162 @@
// 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 super::bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_runtime_run`.
///
/// Obtain a handle from the [`BarkBattleRuntimeRunTableAccess::bark_battle_runtime_run`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_runtime_run()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_runtime_run().on_insert(...)`.
pub struct BarkBattleRuntimeRunTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattleRuntimeRunRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_runtime_run`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattleRuntimeRunTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattleRuntimeRunTableHandle`], which mediates access to the table `bark_battle_runtime_run`.
fn bark_battle_runtime_run(&self) -> BarkBattleRuntimeRunTableHandle<'_>;
}
impl BarkBattleRuntimeRunTableAccess for super::RemoteTables {
fn bark_battle_runtime_run(&self) -> BarkBattleRuntimeRunTableHandle<'_> {
BarkBattleRuntimeRunTableHandle {
imp: self
.imp
.get_table::<BarkBattleRuntimeRunRow>("bark_battle_runtime_run"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattleRuntimeRunInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattleRuntimeRunDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattleRuntimeRunTableHandle<'ctx> {
type Row = BarkBattleRuntimeRunRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattleRuntimeRunRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattleRuntimeRunInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleRuntimeRunInsertCallbackId {
BarkBattleRuntimeRunInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattleRuntimeRunInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattleRuntimeRunDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleRuntimeRunDeleteCallbackId {
BarkBattleRuntimeRunDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattleRuntimeRunDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattleRuntimeRunUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleRuntimeRunTableHandle<'ctx> {
type UpdateCallbackId = BarkBattleRuntimeRunUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattleRuntimeRunUpdateCallbackId {
BarkBattleRuntimeRunUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattleRuntimeRunUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `run_id` unique index on the table `bark_battle_runtime_run`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattleRuntimeRunRunIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_runtime_run().run_id().find(...)`.
pub struct BarkBattleRuntimeRunRunIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattleRuntimeRunRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattleRuntimeRunTableHandle<'ctx> {
/// Get a handle on the `run_id` unique index on the table `bark_battle_runtime_run`.
pub fn run_id(&self) -> BarkBattleRuntimeRunRunIdUnique<'ctx> {
BarkBattleRuntimeRunRunIdUnique {
imp: self.imp.get_unique_constraint::<String>("run_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattleRuntimeRunRunIdUnique<'ctx> {
/// Find the subscribed row whose `run_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<BarkBattleRuntimeRunRow> {
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::<BarkBattleRuntimeRunRow>("bark_battle_runtime_run");
_table.add_unique_constraint::<String>("run_id", |row| &row.run_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleRuntimeRunRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<BarkBattleRuntimeRunRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattleRuntimeRunRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_runtime_runQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattleRuntimeRunRow`.
fn bark_battle_runtime_run(&self) -> __sdk::__query_builder::Table<BarkBattleRuntimeRunRow>;
}
impl bark_battle_runtime_runQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_runtime_run(&self) -> __sdk::__query_builder::Table<BarkBattleRuntimeRunRow> {
__sdk::__query_builder::Table::new("bark_battle_runtime_run")
}
}

View File

@@ -0,0 +1,106 @@
// 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 BarkBattleScoreRecordRow {
pub score_id: String,
pub owner_user_id: String,
pub work_id: String,
pub run_id: String,
pub config_version: u64,
pub ruleset_version: String,
pub difficulty_preset: String,
pub leaderboard_enabled: bool,
pub metrics_json: String,
pub derived_metrics_json: String,
pub server_result: String,
pub validation_status: String,
pub anti_cheat_flags_json: String,
pub leaderboard_score: Option<u64>,
pub recorded_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattleScoreRecordRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattleScoreRecordRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattleScoreRecordRowCols {
pub score_id: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub work_id: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub run_id: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub config_version: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, u64>,
pub ruleset_version: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub difficulty_preset: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub leaderboard_enabled: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, bool>,
pub metrics_json: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub derived_metrics_json: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub server_result: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub validation_status: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub anti_cheat_flags_json: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, String>,
pub leaderboard_score: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, Option<u64>>,
pub recorded_at: __sdk::__query_builder::Col<BarkBattleScoreRecordRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattleScoreRecordRow {
type Cols = BarkBattleScoreRecordRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattleScoreRecordRowCols {
score_id: __sdk::__query_builder::Col::new(table_name, "score_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
run_id: __sdk::__query_builder::Col::new(table_name, "run_id"),
config_version: __sdk::__query_builder::Col::new(table_name, "config_version"),
ruleset_version: __sdk::__query_builder::Col::new(table_name, "ruleset_version"),
difficulty_preset: __sdk::__query_builder::Col::new(table_name, "difficulty_preset"),
leaderboard_enabled: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_enabled",
),
metrics_json: __sdk::__query_builder::Col::new(table_name, "metrics_json"),
derived_metrics_json: __sdk::__query_builder::Col::new(
table_name,
"derived_metrics_json",
),
server_result: __sdk::__query_builder::Col::new(table_name, "server_result"),
validation_status: __sdk::__query_builder::Col::new(table_name, "validation_status"),
anti_cheat_flags_json: __sdk::__query_builder::Col::new(
table_name,
"anti_cheat_flags_json",
),
leaderboard_score: __sdk::__query_builder::Col::new(table_name, "leaderboard_score"),
recorded_at: __sdk::__query_builder::Col::new(table_name, "recorded_at"),
}
}
}
/// Indexed column accessor struct for the table `BarkBattleScoreRecordRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattleScoreRecordRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<BarkBattleScoreRecordRow, String>,
pub run_id: __sdk::__query_builder::IxCol<BarkBattleScoreRecordRow, String>,
pub score_id: __sdk::__query_builder::IxCol<BarkBattleScoreRecordRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattleScoreRecordRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattleScoreRecordRow {
type IxCols = BarkBattleScoreRecordRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattleScoreRecordRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
run_id: __sdk::__query_builder::IxCol::new(table_name, "run_id"),
score_id: __sdk::__query_builder::IxCol::new(table_name, "score_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattleScoreRecordRow {}

View File

@@ -0,0 +1,162 @@
// 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 super::bark_battle_score_record_row_type::BarkBattleScoreRecordRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_score_record`.
///
/// Obtain a handle from the [`BarkBattleScoreRecordTableAccess::bark_battle_score_record`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_score_record()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_score_record().on_insert(...)`.
pub struct BarkBattleScoreRecordTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattleScoreRecordRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_score_record`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattleScoreRecordTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattleScoreRecordTableHandle`], which mediates access to the table `bark_battle_score_record`.
fn bark_battle_score_record(&self) -> BarkBattleScoreRecordTableHandle<'_>;
}
impl BarkBattleScoreRecordTableAccess for super::RemoteTables {
fn bark_battle_score_record(&self) -> BarkBattleScoreRecordTableHandle<'_> {
BarkBattleScoreRecordTableHandle {
imp: self
.imp
.get_table::<BarkBattleScoreRecordRow>("bark_battle_score_record"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattleScoreRecordInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattleScoreRecordDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattleScoreRecordTableHandle<'ctx> {
type Row = BarkBattleScoreRecordRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattleScoreRecordRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattleScoreRecordInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleScoreRecordInsertCallbackId {
BarkBattleScoreRecordInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattleScoreRecordInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattleScoreRecordDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleScoreRecordDeleteCallbackId {
BarkBattleScoreRecordDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattleScoreRecordDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattleScoreRecordUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleScoreRecordTableHandle<'ctx> {
type UpdateCallbackId = BarkBattleScoreRecordUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattleScoreRecordUpdateCallbackId {
BarkBattleScoreRecordUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattleScoreRecordUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `score_id` unique index on the table `bark_battle_score_record`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattleScoreRecordScoreIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_score_record().score_id().find(...)`.
pub struct BarkBattleScoreRecordScoreIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattleScoreRecordRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattleScoreRecordTableHandle<'ctx> {
/// Get a handle on the `score_id` unique index on the table `bark_battle_score_record`.
pub fn score_id(&self) -> BarkBattleScoreRecordScoreIdUnique<'ctx> {
BarkBattleScoreRecordScoreIdUnique {
imp: self.imp.get_unique_constraint::<String>("score_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattleScoreRecordScoreIdUnique<'ctx> {
/// Find the subscribed row whose `score_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<BarkBattleScoreRecordRow> {
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::<BarkBattleScoreRecordRow>("bark_battle_score_record");
_table.add_unique_constraint::<String>("score_id", |row| &row.score_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleScoreRecordRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<BarkBattleScoreRecordRow>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattleScoreRecordRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_score_recordQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattleScoreRecordRow`.
fn bark_battle_score_record(&self) -> __sdk::__query_builder::Table<BarkBattleScoreRecordRow>;
}
impl bark_battle_score_recordQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_score_record(&self) -> __sdk::__query_builder::Table<BarkBattleScoreRecordRow> {
__sdk::__query_builder::Table::new("bark_battle_score_record")
}
}

View File

@@ -0,0 +1,19 @@
// 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 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,
}
impl __sdk::InModule for BarkBattleWorkPublishInput {
type Module = super::RemoteModule;
}

View File

@@ -0,0 +1,111 @@
// 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 BarkBattleWorkStatsProjectionRow {
pub work_id: String,
pub owner_user_id: String,
pub play_count: u64,
pub finished_count: u64,
pub accepted_score_count: u64,
pub leaderboard_entry_count: u64,
pub best_leaderboard_score: Option<u64>,
pub best_score_id: Option<String>,
pub best_run_id: Option<String>,
pub average_final_energy: f32,
pub average_trigger_count: f32,
pub last_finished_at_micros: Option<i64>,
pub stats_json: String,
pub updated_at: __sdk::Timestamp,
}
impl __sdk::InModule for BarkBattleWorkStatsProjectionRow {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `BarkBattleWorkStatsProjectionRow`.
///
/// Provides typed access to columns for query building.
pub struct BarkBattleWorkStatsProjectionRowCols {
pub work_id: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, String>,
pub owner_user_id: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, String>,
pub play_count: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, u64>,
pub finished_count: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, u64>,
pub accepted_score_count: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, u64>,
pub leaderboard_entry_count: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, u64>,
pub best_leaderboard_score:
__sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, Option<u64>>,
pub best_score_id:
__sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, Option<String>>,
pub best_run_id: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, Option<String>>,
pub average_final_energy: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, f32>,
pub average_trigger_count: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, f32>,
pub last_finished_at_micros:
__sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, Option<i64>>,
pub stats_json: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, String>,
pub updated_at: __sdk::__query_builder::Col<BarkBattleWorkStatsProjectionRow, __sdk::Timestamp>,
}
impl __sdk::__query_builder::HasCols for BarkBattleWorkStatsProjectionRow {
type Cols = BarkBattleWorkStatsProjectionRowCols;
fn cols(table_name: &'static str) -> Self::Cols {
BarkBattleWorkStatsProjectionRowCols {
work_id: __sdk::__query_builder::Col::new(table_name, "work_id"),
owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"),
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
finished_count: __sdk::__query_builder::Col::new(table_name, "finished_count"),
accepted_score_count: __sdk::__query_builder::Col::new(
table_name,
"accepted_score_count",
),
leaderboard_entry_count: __sdk::__query_builder::Col::new(
table_name,
"leaderboard_entry_count",
),
best_leaderboard_score: __sdk::__query_builder::Col::new(
table_name,
"best_leaderboard_score",
),
best_score_id: __sdk::__query_builder::Col::new(table_name, "best_score_id"),
best_run_id: __sdk::__query_builder::Col::new(table_name, "best_run_id"),
average_final_energy: __sdk::__query_builder::Col::new(
table_name,
"average_final_energy",
),
average_trigger_count: __sdk::__query_builder::Col::new(
table_name,
"average_trigger_count",
),
last_finished_at_micros: __sdk::__query_builder::Col::new(
table_name,
"last_finished_at_micros",
),
stats_json: __sdk::__query_builder::Col::new(table_name, "stats_json"),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
}
}
}
/// Indexed column accessor struct for the table `BarkBattleWorkStatsProjectionRow`.
///
/// Provides typed access to indexed columns for query building.
pub struct BarkBattleWorkStatsProjectionRowIxCols {
pub owner_user_id: __sdk::__query_builder::IxCol<BarkBattleWorkStatsProjectionRow, String>,
pub work_id: __sdk::__query_builder::IxCol<BarkBattleWorkStatsProjectionRow, String>,
}
impl __sdk::__query_builder::HasIxCols for BarkBattleWorkStatsProjectionRow {
type IxCols = BarkBattleWorkStatsProjectionRowIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
BarkBattleWorkStatsProjectionRowIxCols {
owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"),
work_id: __sdk::__query_builder::IxCol::new(table_name, "work_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for BarkBattleWorkStatsProjectionRow {}

View File

@@ -0,0 +1,169 @@
// 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 super::bark_battle_work_stats_projection_row_type::BarkBattleWorkStatsProjectionRow;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `bark_battle_work_stats_projection`.
///
/// Obtain a handle from the [`BarkBattleWorkStatsProjectionTableAccess::bark_battle_work_stats_projection`] method on [`super::RemoteTables`],
/// like `ctx.db.bark_battle_work_stats_projection()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_work_stats_projection().on_insert(...)`.
pub struct BarkBattleWorkStatsProjectionTableHandle<'ctx> {
imp: __sdk::TableHandle<BarkBattleWorkStatsProjectionRow>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `bark_battle_work_stats_projection`.
///
/// Implemented for [`super::RemoteTables`].
pub trait BarkBattleWorkStatsProjectionTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`BarkBattleWorkStatsProjectionTableHandle`], which mediates access to the table `bark_battle_work_stats_projection`.
fn bark_battle_work_stats_projection(&self) -> BarkBattleWorkStatsProjectionTableHandle<'_>;
}
impl BarkBattleWorkStatsProjectionTableAccess for super::RemoteTables {
fn bark_battle_work_stats_projection(&self) -> BarkBattleWorkStatsProjectionTableHandle<'_> {
BarkBattleWorkStatsProjectionTableHandle {
imp: self
.imp
.get_table::<BarkBattleWorkStatsProjectionRow>("bark_battle_work_stats_projection"),
ctx: std::marker::PhantomData,
}
}
}
pub struct BarkBattleWorkStatsProjectionInsertCallbackId(__sdk::CallbackId);
pub struct BarkBattleWorkStatsProjectionDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for BarkBattleWorkStatsProjectionTableHandle<'ctx> {
type Row = BarkBattleWorkStatsProjectionRow;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = BarkBattleWorkStatsProjectionRow> + '_ {
self.imp.iter()
}
type InsertCallbackId = BarkBattleWorkStatsProjectionInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleWorkStatsProjectionInsertCallbackId {
BarkBattleWorkStatsProjectionInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: BarkBattleWorkStatsProjectionInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = BarkBattleWorkStatsProjectionDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> BarkBattleWorkStatsProjectionDeleteCallbackId {
BarkBattleWorkStatsProjectionDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: BarkBattleWorkStatsProjectionDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct BarkBattleWorkStatsProjectionUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for BarkBattleWorkStatsProjectionTableHandle<'ctx> {
type UpdateCallbackId = BarkBattleWorkStatsProjectionUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> BarkBattleWorkStatsProjectionUpdateCallbackId {
BarkBattleWorkStatsProjectionUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: BarkBattleWorkStatsProjectionUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `work_id` unique index on the table `bark_battle_work_stats_projection`,
/// which allows point queries on the field of the same name
/// via the [`BarkBattleWorkStatsProjectionWorkIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.bark_battle_work_stats_projection().work_id().find(...)`.
pub struct BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<BarkBattleWorkStatsProjectionRow, String>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> BarkBattleWorkStatsProjectionTableHandle<'ctx> {
/// Get a handle on the `work_id` unique index on the table `bark_battle_work_stats_projection`.
pub fn work_id(&self) -> BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> {
BarkBattleWorkStatsProjectionWorkIdUnique {
imp: self.imp.get_unique_constraint::<String>("work_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> BarkBattleWorkStatsProjectionWorkIdUnique<'ctx> {
/// Find the subscribed row whose `work_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<BarkBattleWorkStatsProjectionRow> {
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::<BarkBattleWorkStatsProjectionRow>("bark_battle_work_stats_projection");
_table.add_unique_constraint::<String>("work_id", |row| &row.work_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<BarkBattleWorkStatsProjectionRow>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse(
"TableUpdate<BarkBattleWorkStatsProjectionRow>",
"TableUpdate",
)
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `BarkBattleWorkStatsProjectionRow`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait bark_battle_work_stats_projectionQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `BarkBattleWorkStatsProjectionRow`.
fn bark_battle_work_stats_projection(
&self,
) -> __sdk::__query_builder::Table<BarkBattleWorkStatsProjectionRow>;
}
impl bark_battle_work_stats_projectionQueryTableAccess for __sdk::QueryTableAccessor {
fn bark_battle_work_stats_projection(
&self,
) -> __sdk::__query_builder::Table<BarkBattleWorkStatsProjectionRow> {
__sdk::__query_builder::Table::new("bark_battle_work_stats_projection")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,6 +92,28 @@ pub mod auth_store_snapshot_table;
pub mod auth_store_snapshot_type;
pub mod auth_store_snapshot_upsert_input_type;
pub mod authorize_database_migration_operator_procedure;
pub mod bark_battle_draft_config_row_type;
pub mod bark_battle_draft_config_table;
pub mod bark_battle_draft_config_upsert_input_type;
pub mod bark_battle_draft_create_input_type;
pub mod bark_battle_leaderboard_entry_row_type;
pub mod bark_battle_leaderboard_entry_table;
pub mod bark_battle_personal_best_projection_row_type;
pub mod bark_battle_personal_best_projection_table;
pub mod bark_battle_procedure_result_type;
pub mod bark_battle_published_config_row_type;
pub mod bark_battle_published_config_table;
pub mod bark_battle_run_finish_input_type;
pub mod bark_battle_run_get_input_type;
pub mod bark_battle_run_start_input_type;
pub mod bark_battle_runtime_config_get_input_type;
pub mod bark_battle_runtime_run_row_type;
pub mod bark_battle_runtime_run_table;
pub mod bark_battle_score_record_row_type;
pub mod bark_battle_score_record_table;
pub mod bark_battle_work_publish_input_type;
pub mod bark_battle_work_stats_projection_row_type;
pub mod bark_battle_work_stats_projection_table;
pub mod battle_mode_type;
pub mod battle_state_input_type;
pub mod battle_state_procedure_result_type;
@@ -181,6 +203,7 @@ pub mod continue_story_and_return_procedure;
pub mod continue_story_reducer;
pub mod create_ai_task_and_return_procedure;
pub mod create_ai_task_reducer;
pub mod create_bark_battle_draft_procedure;
pub mod create_battle_state_and_return_procedure;
pub mod create_battle_state_reducer;
pub mod create_big_fish_session_procedure;
@@ -298,10 +321,13 @@ pub mod finalize_match_3_d_agent_message_turn_procedure;
pub mod finalize_puzzle_agent_message_turn_procedure;
pub mod finalize_square_hole_agent_message_turn_procedure;
pub mod finalize_visual_novel_agent_message_turn_procedure;
pub mod finish_bark_battle_run_procedure;
pub mod finish_match_3_d_time_up_procedure;
pub mod finish_square_hole_time_up_procedure;
pub mod generate_big_fish_asset_procedure;
pub mod get_auth_store_snapshot_procedure;
pub mod get_bark_battle_run_procedure;
pub mod get_bark_battle_runtime_config_procedure;
pub mod get_battle_state_procedure;
pub mod get_big_fish_run_procedure;
pub mod get_big_fish_session_procedure;
@@ -454,6 +480,7 @@ pub mod public_work_like_table;
pub mod public_work_like_type;
pub mod public_work_play_daily_stat_table;
pub mod public_work_play_daily_stat_type;
pub mod publish_bark_battle_work_procedure;
pub mod publish_big_fish_game_procedure;
pub mod publish_custom_world_profile_and_return_procedure;
pub mod publish_custom_world_profile_reducer;
@@ -719,6 +746,7 @@ pub mod square_hole_works_list_input_type;
pub mod square_hole_works_procedure_result_type;
pub mod start_ai_task_reducer;
pub mod start_ai_task_stage_reducer;
pub mod start_bark_battle_run_procedure;
pub mod start_big_fish_run_procedure;
pub mod start_match_3_d_run_procedure;
pub mod start_puzzle_run_procedure;
@@ -763,6 +791,7 @@ pub mod turn_in_quest_reducer;
pub mod unequip_inventory_item_input_type;
pub mod unpublish_custom_world_profile_and_return_procedure;
pub mod unpublish_custom_world_profile_reducer;
pub mod update_bark_battle_draft_config_procedure;
pub mod update_match_3_d_work_procedure;
pub mod update_puzzle_run_pause_procedure;
pub mod update_puzzle_work_procedure;
@@ -907,6 +936,28 @@ pub use auth_store_snapshot_table::*;
pub use auth_store_snapshot_type::AuthStoreSnapshot;
pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput;
pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator;
pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow;
pub use bark_battle_draft_config_table::*;
pub use bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput;
pub use bark_battle_draft_create_input_type::BarkBattleDraftCreateInput;
pub use bark_battle_leaderboard_entry_row_type::BarkBattleLeaderboardEntryRow;
pub use bark_battle_leaderboard_entry_table::*;
pub use bark_battle_personal_best_projection_row_type::BarkBattlePersonalBestProjectionRow;
pub use bark_battle_personal_best_projection_table::*;
pub use bark_battle_procedure_result_type::BarkBattleProcedureResult;
pub use bark_battle_published_config_row_type::BarkBattlePublishedConfigRow;
pub use bark_battle_published_config_table::*;
pub use bark_battle_run_finish_input_type::BarkBattleRunFinishInput;
pub use bark_battle_run_get_input_type::BarkBattleRunGetInput;
pub use bark_battle_run_start_input_type::BarkBattleRunStartInput;
pub use bark_battle_runtime_config_get_input_type::BarkBattleRuntimeConfigGetInput;
pub use bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow;
pub use bark_battle_runtime_run_table::*;
pub use bark_battle_score_record_row_type::BarkBattleScoreRecordRow;
pub use bark_battle_score_record_table::*;
pub use bark_battle_work_publish_input_type::BarkBattleWorkPublishInput;
pub use bark_battle_work_stats_projection_row_type::BarkBattleWorkStatsProjectionRow;
pub use bark_battle_work_stats_projection_table::*;
pub use battle_mode_type::BattleMode;
pub use battle_state_input_type::BattleStateInput;
pub use battle_state_procedure_result_type::BattleStateProcedureResult;
@@ -996,6 +1047,7 @@ pub use continue_story_and_return_procedure::continue_story_and_return;
pub use continue_story_reducer::continue_story;
pub use create_ai_task_and_return_procedure::create_ai_task_and_return;
pub use create_ai_task_reducer::create_ai_task;
pub use create_bark_battle_draft_procedure::create_bark_battle_draft;
pub use create_battle_state_and_return_procedure::create_battle_state_and_return;
pub use create_battle_state_reducer::create_battle_state;
pub use create_big_fish_session_procedure::create_big_fish_session;
@@ -1113,10 +1165,13 @@ pub use finalize_match_3_d_agent_message_turn_procedure::finalize_match_3_d_agen
pub use finalize_puzzle_agent_message_turn_procedure::finalize_puzzle_agent_message_turn;
pub use finalize_square_hole_agent_message_turn_procedure::finalize_square_hole_agent_message_turn;
pub use finalize_visual_novel_agent_message_turn_procedure::finalize_visual_novel_agent_message_turn;
pub use finish_bark_battle_run_procedure::finish_bark_battle_run;
pub use finish_match_3_d_time_up_procedure::finish_match_3_d_time_up;
pub use finish_square_hole_time_up_procedure::finish_square_hole_time_up;
pub use generate_big_fish_asset_procedure::generate_big_fish_asset;
pub use get_auth_store_snapshot_procedure::get_auth_store_snapshot;
pub use get_bark_battle_run_procedure::get_bark_battle_run;
pub use get_bark_battle_runtime_config_procedure::get_bark_battle_runtime_config;
pub use get_battle_state_procedure::get_battle_state;
pub use get_big_fish_run_procedure::get_big_fish_run;
pub use get_big_fish_session_procedure::get_big_fish_session;
@@ -1269,6 +1324,7 @@ pub use public_work_like_table::*;
pub use public_work_like_type::PublicWorkLike;
pub use public_work_play_daily_stat_table::*;
pub use public_work_play_daily_stat_type::PublicWorkPlayDailyStat;
pub use publish_bark_battle_work_procedure::publish_bark_battle_work;
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_profile_reducer::publish_custom_world_profile;
@@ -1534,6 +1590,7 @@ pub use square_hole_works_list_input_type::SquareHoleWorksListInput;
pub use square_hole_works_procedure_result_type::SquareHoleWorksProcedureResult;
pub use start_ai_task_reducer::start_ai_task;
pub use start_ai_task_stage_reducer::start_ai_task_stage;
pub use start_bark_battle_run_procedure::start_bark_battle_run;
pub use start_big_fish_run_procedure::start_big_fish_run;
pub use start_match_3_d_run_procedure::start_match_3_d_run;
pub use start_puzzle_run_procedure::start_puzzle_run;
@@ -1578,6 +1635,7 @@ pub use turn_in_quest_reducer::turn_in_quest;
pub use unequip_inventory_item_input_type::UnequipInventoryItemInput;
pub use unpublish_custom_world_profile_and_return_procedure::unpublish_custom_world_profile_and_return;
pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile;
pub use update_bark_battle_draft_config_procedure::update_bark_battle_draft_config;
pub use update_match_3_d_work_procedure::update_match_3_d_work;
pub use update_puzzle_run_pause_procedure::update_puzzle_run_pause;
pub use update_puzzle_work_procedure::update_puzzle_work;
@@ -1920,6 +1978,13 @@ pub struct DbUpdate {
auth_identity: __sdk::TableUpdate<AuthIdentity>,
auth_store_projection_meta: __sdk::TableUpdate<AuthStoreProjectionMeta>,
auth_store_snapshot: __sdk::TableUpdate<AuthStoreSnapshot>,
bark_battle_draft_config: __sdk::TableUpdate<BarkBattleDraftConfigRow>,
bark_battle_leaderboard_entry: __sdk::TableUpdate<BarkBattleLeaderboardEntryRow>,
bark_battle_personal_best_projection: __sdk::TableUpdate<BarkBattlePersonalBestProjectionRow>,
bark_battle_published_config: __sdk::TableUpdate<BarkBattlePublishedConfigRow>,
bark_battle_runtime_run: __sdk::TableUpdate<BarkBattleRuntimeRunRow>,
bark_battle_score_record: __sdk::TableUpdate<BarkBattleScoreRecordRow>,
bark_battle_work_stats_projection: __sdk::TableUpdate<BarkBattleWorkStatsProjectionRow>,
battle_state: __sdk::TableUpdate<BattleState>,
big_fish_agent_message: __sdk::TableUpdate<BigFishAgentMessage>,
big_fish_asset_slot: __sdk::TableUpdate<BigFishAssetSlot>,
@@ -2033,6 +2098,33 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"auth_store_snapshot" => db_update
.auth_store_snapshot
.append(auth_store_snapshot_table::parse_table_update(table_update)?),
"bark_battle_draft_config" => db_update.bark_battle_draft_config.append(
bark_battle_draft_config_table::parse_table_update(table_update)?,
),
"bark_battle_leaderboard_entry" => db_update.bark_battle_leaderboard_entry.append(
bark_battle_leaderboard_entry_table::parse_table_update(table_update)?,
),
"bark_battle_personal_best_projection" => {
db_update.bark_battle_personal_best_projection.append(
bark_battle_personal_best_projection_table::parse_table_update(
table_update,
)?,
)
}
"bark_battle_published_config" => db_update.bark_battle_published_config.append(
bark_battle_published_config_table::parse_table_update(table_update)?,
),
"bark_battle_runtime_run" => db_update.bark_battle_runtime_run.append(
bark_battle_runtime_run_table::parse_table_update(table_update)?,
),
"bark_battle_score_record" => db_update.bark_battle_score_record.append(
bark_battle_score_record_table::parse_table_update(table_update)?,
),
"bark_battle_work_stats_projection" => {
db_update.bark_battle_work_stats_projection.append(
bark_battle_work_stats_projection_table::parse_table_update(table_update)?,
)
}
"battle_state" => db_update
.battle_state
.append(battle_state_table::parse_table_update(table_update)?),
@@ -2317,6 +2409,48 @@ impl __sdk::DbUpdate for DbUpdate {
&self.auth_store_snapshot,
)
.with_updates_by_pk(|row| &row.snapshot_id);
diff.bark_battle_draft_config = cache
.apply_diff_to_table::<BarkBattleDraftConfigRow>(
"bark_battle_draft_config",
&self.bark_battle_draft_config,
)
.with_updates_by_pk(|row| &row.draft_id);
diff.bark_battle_leaderboard_entry = cache
.apply_diff_to_table::<BarkBattleLeaderboardEntryRow>(
"bark_battle_leaderboard_entry",
&self.bark_battle_leaderboard_entry,
)
.with_updates_by_pk(|row| &row.leaderboard_entry_id);
diff.bark_battle_personal_best_projection = cache
.apply_diff_to_table::<BarkBattlePersonalBestProjectionRow>(
"bark_battle_personal_best_projection",
&self.bark_battle_personal_best_projection,
)
.with_updates_by_pk(|row| &row.personal_best_id);
diff.bark_battle_published_config = cache
.apply_diff_to_table::<BarkBattlePublishedConfigRow>(
"bark_battle_published_config",
&self.bark_battle_published_config,
)
.with_updates_by_pk(|row| &row.work_id);
diff.bark_battle_runtime_run = cache
.apply_diff_to_table::<BarkBattleRuntimeRunRow>(
"bark_battle_runtime_run",
&self.bark_battle_runtime_run,
)
.with_updates_by_pk(|row| &row.run_id);
diff.bark_battle_score_record = cache
.apply_diff_to_table::<BarkBattleScoreRecordRow>(
"bark_battle_score_record",
&self.bark_battle_score_record,
)
.with_updates_by_pk(|row| &row.score_id);
diff.bark_battle_work_stats_projection = cache
.apply_diff_to_table::<BarkBattleWorkStatsProjectionRow>(
"bark_battle_work_stats_projection",
&self.bark_battle_work_stats_projection,
)
.with_updates_by_pk(|row| &row.work_id);
diff.battle_state = cache
.apply_diff_to_table::<BattleState>("battle_state", &self.battle_state)
.with_updates_by_pk(|row| &row.battle_state_id);
@@ -2717,6 +2851,27 @@ impl __sdk::DbUpdate for DbUpdate {
"auth_store_snapshot" => db_update
.auth_store_snapshot
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_draft_config" => db_update
.bark_battle_draft_config
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_leaderboard_entry" => db_update
.bark_battle_leaderboard_entry
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_personal_best_projection" => db_update
.bark_battle_personal_best_projection
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_published_config" => db_update
.bark_battle_published_config
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_runtime_run" => db_update
.bark_battle_runtime_run
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_score_record" => db_update
.bark_battle_score_record
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"bark_battle_work_stats_projection" => db_update
.bark_battle_work_stats_projection
.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)?),
@@ -2973,6 +3128,27 @@ impl __sdk::DbUpdate for DbUpdate {
"auth_store_snapshot" => db_update
.auth_store_snapshot
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_draft_config" => db_update
.bark_battle_draft_config
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_leaderboard_entry" => db_update
.bark_battle_leaderboard_entry
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_personal_best_projection" => db_update
.bark_battle_personal_best_projection
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_published_config" => db_update
.bark_battle_published_config
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_runtime_run" => db_update
.bark_battle_runtime_run
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_score_record" => db_update
.bark_battle_score_record
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"bark_battle_work_stats_projection" => db_update
.bark_battle_work_stats_projection
.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)?),
@@ -3207,6 +3383,15 @@ pub struct AppliedDiff<'r> {
auth_identity: __sdk::TableAppliedDiff<'r, AuthIdentity>,
auth_store_projection_meta: __sdk::TableAppliedDiff<'r, AuthStoreProjectionMeta>,
auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>,
bark_battle_draft_config: __sdk::TableAppliedDiff<'r, BarkBattleDraftConfigRow>,
bark_battle_leaderboard_entry: __sdk::TableAppliedDiff<'r, BarkBattleLeaderboardEntryRow>,
bark_battle_personal_best_projection:
__sdk::TableAppliedDiff<'r, BarkBattlePersonalBestProjectionRow>,
bark_battle_published_config: __sdk::TableAppliedDiff<'r, BarkBattlePublishedConfigRow>,
bark_battle_runtime_run: __sdk::TableAppliedDiff<'r, BarkBattleRuntimeRunRow>,
bark_battle_score_record: __sdk::TableAppliedDiff<'r, BarkBattleScoreRecordRow>,
bark_battle_work_stats_projection:
__sdk::TableAppliedDiff<'r, BarkBattleWorkStatsProjectionRow>,
battle_state: __sdk::TableAppliedDiff<'r, BattleState>,
big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>,
big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>,
@@ -3342,6 +3527,41 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.auth_store_snapshot,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattleDraftConfigRow>(
"bark_battle_draft_config",
&self.bark_battle_draft_config,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattleLeaderboardEntryRow>(
"bark_battle_leaderboard_entry",
&self.bark_battle_leaderboard_entry,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattlePersonalBestProjectionRow>(
"bark_battle_personal_best_projection",
&self.bark_battle_personal_best_projection,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattlePublishedConfigRow>(
"bark_battle_published_config",
&self.bark_battle_published_config,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattleRuntimeRunRow>(
"bark_battle_runtime_run",
&self.bark_battle_runtime_run,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattleScoreRecordRow>(
"bark_battle_score_record",
&self.bark_battle_score_record,
event,
);
callbacks.invoke_table_row_callbacks::<BarkBattleWorkStatsProjectionRow>(
"bark_battle_work_stats_projection",
&self.bark_battle_work_stats_projection,
event,
);
callbacks.invoke_table_row_callbacks::<BattleState>(
"battle_state",
&self.battle_state,
@@ -4347,6 +4567,13 @@ impl __sdk::SpacetimeModule for RemoteModule {
auth_identity_table::register_table(client_cache);
auth_store_projection_meta_table::register_table(client_cache);
auth_store_snapshot_table::register_table(client_cache);
bark_battle_draft_config_table::register_table(client_cache);
bark_battle_leaderboard_entry_table::register_table(client_cache);
bark_battle_personal_best_projection_table::register_table(client_cache);
bark_battle_published_config_table::register_table(client_cache);
bark_battle_runtime_run_table::register_table(client_cache);
bark_battle_score_record_table::register_table(client_cache);
bark_battle_work_stats_projection_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);
@@ -4430,6 +4657,13 @@ impl __sdk::SpacetimeModule for RemoteModule {
"auth_identity",
"auth_store_projection_meta",
"auth_store_snapshot",
"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",
"battle_state",
"big_fish_agent_message",
"big_fish_asset_slot",

View File

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

View File

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

View File

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

View File

@@ -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"] }

View 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());
}
}

View 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,
}

View 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>,
}

View File

@@ -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::*;

View File

@@ -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,
@@ -217,6 +222,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,

View File

@@ -213,119 +213,18 @@ fn migrate_visual_novel_entry_from_old_visible_default(ctx: &ReducerContext, now
}
fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeConfig> {
vec![
build_creation_entry_type_seed(
"rpg",
"文字冒险",
"经典 RPG 体验",
"内测",
"/creation-type-references/rpg.webp",
false,
true,
10,
now,
),
build_creation_entry_type_seed(
"big-fish",
"摸鱼",
"轻量闯关玩法",
"可创建",
"/creation-type-references/big-fish.webp",
false,
true,
20,
now,
),
build_creation_entry_type_seed(
"puzzle",
"拼图",
"拼图关卡创作",
"可创建",
"/creation-type-references/puzzle.webp",
true,
true,
30,
now,
),
build_creation_entry_type_seed(
"match3d",
"抓大鹅",
"3D 消除关卡",
"可创建",
"/creation-type-references/match3d.webp",
true,
true,
40,
now,
),
build_creation_entry_type_seed(
"square-hole",
"方洞",
"形状投放挑战",
"可创建",
"/creation-type-references/square-hole.webp",
false,
true,
50,
now,
),
build_creation_entry_type_seed(
"visual-novel",
"视觉小说",
"分支叙事体验",
"敬请期待",
"/creation-type-references/visual-novel.webp",
false,
false,
60,
now,
),
build_creation_entry_type_seed(
"airp",
"AI RPG",
"原生角色扮演",
"即将开放",
"/creation-type-references/airp.webp",
true,
false,
70,
now,
),
build_creation_entry_type_seed(
"creative-agent",
"智能体创作",
"对话式创作实验",
"内测",
"/creation-type-references/creative-agent.webp",
false,
true,
80,
now,
),
]
}
#[allow(clippy::too_many_arguments)]
fn build_creation_entry_type_seed(
id: &str,
title: &str,
subtitle: &str,
badge: &str,
image_src: &str,
visible: bool,
open: bool,
sort_order: i32,
now: Timestamp,
) -> CreationEntryTypeConfig {
CreationEntryTypeConfig {
id: id.to_string(),
title: title.to_string(),
subtitle: subtitle.to_string(),
badge: badge.to_string(),
image_src: image_src.to_string(),
visible,
open,
sort_order,
updated_at: now,
}
module_runtime::default_creation_entry_type_snapshots(now.to_micros_since_unix_epoch())
.into_iter()
.map(|snapshot| CreationEntryTypeConfig {
id: snapshot.id,
title: snapshot.title,
subtitle: snapshot.subtitle,
badge: snapshot.badge,
image_src: snapshot.image_src,
visible: snapshot.visible,
open: snapshot.open,
sort_order: snapshot.sort_order,
updated_at: now,
})
.collect()
}

View File

@@ -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::*;