Merge remote-tracking branch 'origin/master' into feat/pricing-plan
Project CI / Backend tests (pull_request) Failing after 48s
Project CI / Native shell tests (pull_request) Failing after 3m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m21s
Project CI / Frontend tests (pull_request) Failing after 47s
Project CI / AI game creator shell web tests (pull_request) Failing after 39s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m6s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m23s
Project CI / Repository checks (pull_request) Successful in 6m31s

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-10-05 10:50:30 +08:00
165 changed files with 13827 additions and 1670 deletions
@@ -16,8 +16,8 @@ use crate::{
pub struct FrontendRuntimeConfigResponse {
pub image_editor_agent_sidebar_enabled: bool,
pub agc_template_library_enabled: bool,
/// 游戏发布灰度:运营未配置 `game-distribution:publish` 时对已登录作者默认开放,
/// 显式收紧后只有白名单/灰度命中的作者拿到 `true`;匿名一律 `false`(发布必须先登录)。
/// 游戏发布灰度:未配置 `game-distribution:publish` 或 `enabled=false` 时不开放;
/// 显式配置后按 enabled、白名单、标签和灰度比例判定,匿名一律 `false`(发布必须先登录)。
pub game_distribution_publish_enabled: bool,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
(function () {
function makeStorage() {
var values = Object.create(null);
return {
get length() {
return Object.keys(values).length;
},
key: function (index) {
return Object.keys(values)[index] || null;
},
getItem: function (key) {
key = String(key);
return Object.prototype.hasOwnProperty.call(values, key)
? values[key]
: null;
},
setItem: function (key, value) {
values[String(key)] = String(value);
},
removeItem: function (key) {
delete values[String(key)];
},
clear: function () {
values = Object.create(null);
},
};
}
function install(name) {
try {
Object.defineProperty(window, name, {
configurable: true,
enumerable: true,
value: makeStorage(),
});
} catch (_) {
// opaque-origin native storage may reject property replacement; keep the sandbox intact.
}
}
install('localStorage');
install('sessionStorage');
})();
+39 -1
View File
@@ -3,7 +3,7 @@ use std::sync::Mutex;
#[cfg(test)]
use std::sync::atomic::AtomicUsize;
use std::{
collections::BTreeMap,
collections::{BTreeMap, HashMap},
error::Error,
fmt,
sync::{
@@ -137,6 +137,12 @@ impl BackpressureState {
}
}
#[derive(Clone, Debug)]
pub(crate) struct GameDistributionPreviewSession {
pub(crate) version_id: String,
pub(crate) expires_at: OffsetDateTime,
}
#[derive(Clone)]
pub struct AppState(Arc<AppStateInner>);
@@ -307,6 +313,8 @@ pub struct AppStateInner {
ai_task_service: AiTaskService,
spacetime_client: SpacetimeClient,
external_api_auth_state: ExternalApiAuthState,
game_distribution_preview_sessions:
Arc<AsyncMutex<HashMap<String, GameDistributionPreviewSession>>>,
editor_project_state: EditorProjectState,
#[cfg(any())]
puzzle_gallery_cache: PuzzleGalleryCache,
@@ -698,6 +706,7 @@ impl AppState {
test_fail_editor_generation_enqueue: AtomicBool::new(false),
#[cfg(test)]
test_external_background_removal_enqueue: Arc::new(Mutex::new(None)),
game_distribution_preview_sessions: Arc::new(AsyncMutex::new(HashMap::new())),
oss_client,
project_snapshot_oss_client,
template_library_store,
@@ -759,6 +768,35 @@ impl AppState {
&self.refresh_cookie_config
}
pub(crate) async fn create_game_distribution_preview_session(
&self,
version_id: String,
expires_at: OffsetDateTime,
) -> String {
let token = uuid::Uuid::new_v4().to_string();
let now = OffsetDateTime::now_utc();
let mut sessions = self.game_distribution_preview_sessions.lock().await;
sessions.retain(|_, session| session.expires_at > now);
sessions.insert(
token.clone(),
GameDistributionPreviewSession {
version_id,
expires_at,
},
);
token
}
pub(crate) async fn get_game_distribution_preview_session(
&self,
token: &str,
) -> Option<GameDistributionPreviewSession> {
let now = OffsetDateTime::now_utc();
let mut sessions = self.game_distribution_preview_sessions.lock().await;
sessions.retain(|_, session| session.expires_at > now);
sessions.get(token).cloned()
}
pub fn http_request_permit_pools(&self) -> HttpRequestPermitPools {
self.http_request_permit_pools.clone()
}
@@ -7,7 +7,7 @@ use crate::{
commands::{CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision},
domain::{
GameDistributionAction, GameSnapshot, GameVersionSnapshot, GameVersionStatus,
GameVisibility, normalize_id,
GameVisibility, normalize_id, resolve_version_number,
},
errors::{GameDistributionError, GameDistributionFieldError},
};
@@ -146,14 +146,16 @@ impl GameDistributionService {
if state.versions.contains_key(&version_id) {
return Err(GameDistributionError::VersionAlreadyExists);
}
let version_number = state
.versions
.values()
.filter(|version| version.game_id == game_id)
.map(|version| version.version_number)
.max()
.unwrap_or(0)
.saturating_add(1);
let version_number = resolve_version_number(
state
.versions
.values()
.filter(|version| version.game_id == game_id)
.map(|version| version.version_number)
.max()
.unwrap_or(0),
input.version_number,
)?;
let snapshot = GameVersionSnapshot {
version_id: version_id.clone(),
game_id,
@@ -880,6 +882,7 @@ mod tests {
game_id: "game-1".to_string(),
owner_user_id: "owner-1".to_string(),
version_id: "version-1".to_string(),
version_number: None,
package_sha256: "a".repeat(64),
package_bytes: 42,
idempotency: idem("create-version-1", "version-1"),
@@ -1159,6 +1162,7 @@ mod tests {
game_id: "game-1".to_string(),
owner_user_id: "owner-1".to_string(),
version_id: "version-2".to_string(),
version_number: None,
package_sha256: "b".repeat(64),
package_bytes: 7,
idempotency: idem("create-version-2", "version-2"),
@@ -1270,4 +1274,61 @@ mod tests {
assert_eq!(snapshot.status, GameVersionStatus::Uploaded);
assert_eq!(repeated.snapshot, snapshot);
}
#[test]
fn explicit_version_number_accepts_repeats_and_reverts_of_the_same_label() {
assert_eq!(resolve_version_number(0, Some(1)).expect("首版显式 1"), 1);
// 重复标签:线上已有 3 也要能再提交一次 3,并生成新的版本记录。
assert_eq!(resolve_version_number(3, Some(3)).expect("重复标签"), 3);
// 回退:线上最大 5,也允许提交 2。
assert_eq!(resolve_version_number(5, Some(2)).expect("回退标签"), 2);
assert_eq!(
resolve_version_number(0, None).expect("旧客户端自动递增"),
1
);
assert_eq!(
resolve_version_number(7, None).expect("旧客户端按最大值递增"),
8
);
}
#[test]
fn version_number_must_be_positive_and_auto_increment_fails_closed() {
assert_eq!(
resolve_version_number(0, Some(0)).expect_err("0 不是合法标签"),
GameDistributionFieldError::InvalidVersionNumber
);
assert_eq!(
resolve_version_number(u64::MAX, None).expect_err("自动递增溢出必须失败关闭"),
GameDistributionFieldError::VersionNumberExhausted
);
}
#[test]
fn repeating_the_same_label_creates_a_new_version_record() {
let service = service();
let first = create_version(&service);
assert_eq!(first.version_number, 1);
// 同一个版本标签再提交一次:旧的已公开实例不原地修改,而是新增一条版本记录。
let repeated = service
.create_version(CreateVersionInput {
game_id: "game-1".to_string(),
owner_user_id: "owner-1".to_string(),
version_id: "version-1-again".to_string(),
version_number: Some(first.version_number),
package_sha256: "b".repeat(64),
package_bytes: 9,
idempotency: idem("create-version-again", "version-again"),
created_at_micros: 12,
})
.expect("同一个版本标签可以重复提交")
.snapshot;
assert_eq!(repeated.version_number, first.version_number);
assert_ne!(repeated.version_id, first.version_id);
assert_eq!(
service.get_version(&first.version_id).expect("旧版本仍在"),
first
);
}
}
@@ -34,6 +34,10 @@ pub struct CreateVersionInput {
pub game_id: String,
pub owner_user_id: String,
pub version_id: String,
/// 用户可见的正整数版本标签;`None` 时按该游戏已有最大版本号 +1(旧客户端兼容)。
/// 传入时只要求 `>= 1`:同一 gameId 的同一标签可以反复提交,每次都是一条新版本记录。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_number: Option<u64>,
pub package_sha256: String,
pub package_bytes: u64,
pub idempotency: IdempotencyRequest,
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::errors::GameDistributionFieldError;
use shared_kernel::{build_prefixed_seed_id, normalize_required_string};
pub const GAME_ID_PREFIX: &str = "game_";
@@ -17,6 +18,25 @@ pub fn generate_game_version_id(seed_micros: i64, version_number: u64) -> String
)
}
/// 解析本次创建的版本号(用户可见的版本标签,不是不可变的 `version_id`)。
///
/// - `None`:旧客户端兼容路径,取该游戏已有最大版本号 +1;溢出时失败关闭,不回绕成 0。
/// - `Some(requested)`:只要求正整数。同一个 gameId 的同一个版本号可以反复提交——每次提交都是
/// 一条新的版本记录(新的 `version_id`),旧公开实例不原地修改——所以**不与已有最大值比较**,
/// 允许重复标签,也允许回退到更小的标签。
pub fn resolve_version_number(
max_existing: u64,
requested: Option<u64>,
) -> Result<u64, GameDistributionFieldError> {
match requested {
Some(0) => Err(GameDistributionFieldError::InvalidVersionNumber),
Some(requested) => Ok(requested),
None => max_existing
.checked_add(1)
.ok_or(GameDistributionFieldError::VersionNumberExhausted),
}
}
/// Computes the lowercase SHA-256 digest used by the idempotency contract.
pub fn compute_request_digest(payload: impl AsRef<[u8]>) -> String {
let digest = Sha256::digest(payload.as_ref());
@@ -13,6 +13,8 @@ pub enum GameDistributionFieldError {
InvalidPackageDigest,
InvalidPackageBytes,
MissingReviewReason,
InvalidVersionNumber,
VersionNumberExhausted,
}
impl fmt::Display for GameDistributionFieldError {
@@ -27,6 +29,8 @@ impl fmt::Display for GameDistributionFieldError {
Self::InvalidPackageDigest => "packageSha256 必须是 64 位十六进制摘要",
Self::InvalidPackageBytes => "packageBytes 必须大于 0",
Self::MissingReviewReason => "拒绝审核必须提供理由",
Self::InvalidVersionNumber => "版本号必须是大于 0 的整数",
Self::VersionNumberExhausted => "版本号已达上限,无法自动递增",
};
formatter.write_str(message)
}
@@ -21,7 +21,7 @@ pub use application::{
pub use commands::{CreateGameInput, CreateVersionInput, IdempotencyRequest, ReviewDecision};
pub use domain::{
GameDistributionAction, GameSnapshot, GameVersionSnapshot, GameVersionStatus, GameVisibility,
compute_request_digest, generate_game_id, generate_game_version_id,
compute_request_digest, generate_game_id, generate_game_version_id, resolve_version_number,
};
pub use errors::{GameDistributionError, GameDistributionFieldError};
pub use events::GameDistributionEvent;
@@ -87,7 +87,7 @@ pub fn creation_entry_feature_gate_key(creation_type_id: &str) -> String {
pub const IMAGE_EDITOR_AGENT_SIDEBAR_GATE_KEY: &str = "image-editor:agent-sidebar";
pub const AGC_TEMPLATE_LIBRARY_GATE_KEY: &str = "agc:template-library";
/// 游戏分发发布开关:`gate 行缺失`或 `enabled=false` 都表示**未开放**(灰度默认关闭);
/// 游戏分发发布开关:gate 行缺失或 `enabled=false` 都表示**未开放**;
/// 只有 `enabled=true` 且白名单 / 灰度比例 / 用户标签命中时才允许发布。
pub const GAME_DISTRIBUTION_PUBLISH_GATE_KEY: &str = "game-distribution:publish";
@@ -1070,6 +1070,23 @@ pub fn validate_game_iteration_versions(versions: &[GameIterationVersion]) -> Re
/// 代价与配套:未知顶层字段本身就是「这个文件比本客户端新」的信号。客户端读到直接报错并拒绝改写,
/// 由调用方提示升级;同一读路径上还有 `schemaVersion` 的失败关闭门,两者互补——`schemaVersion` 管
/// 「文件明说自己是新版本」,本属性管「版本号没升但字段已经变了」。
/// AGC 本地发布绑定:记录该本地项目对应哪个平台账号/origin 的哪个游戏与最近版本。
/// 缺失即未发布;accountId/apiBaseUrl 用于按实际会话隔离,避免切换账号后接管他人作品。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameCreationAppPublicationBinding {
pub account_id: String,
pub api_base_url: String,
pub game_id: String,
pub revision: u64,
#[serde(default)]
pub latest_version_id: Option<String>,
#[serde(default)]
pub latest_version_number: Option<u64>,
#[serde(default)]
pub status: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GameCreationAppManifest {
@@ -1091,6 +1108,15 @@ pub struct GameCreationAppManifest {
pub godot_project_root: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cocos_project_root: Option<String>,
/// 遗留位:历史清单写过的用户发行版本标签,仅为兼容旧清单可解析而保留。
///
/// 它**不再**是发行版本来源:发布面板的「项目版本」由 AGC 工程内部版本记录 `versions[]`
/// 派生(标签 = 存活内部版本条数),原生不再写入或回填这个字段。字段本身不能删除:
/// 本结构体带 `deny_unknown_fields`,删掉它会让所有含该键的存量清单解析失败。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_version: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub publication: Option<GameCreationAppPublicationBinding>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@@ -1224,6 +1250,8 @@ pub fn new_game_creation_app_manifest(
command_runs: Vec::new(),
versions: Vec::new(),
godot_project_root: None,
project_version: None,
publication: None,
cocos_project_root: None,
}
}
@@ -2162,6 +2190,50 @@ mod tests {
assert_eq!(payload["godotProjectRoot"], json!("game-source"));
}
/// `projectVersion` 降级为遗留兼容位:存量清单里带这个键时必须照旧可读、可往返
/// (本结构体带 `deny_unknown_fields`,拒掉它等于让老项目打不开),但它不再是发行版本
/// 来源——Rust 侧不再有任何解析或回填入口,发布面板的「项目版本」改由工程内部版本
/// `versions[]` 派生。
#[test]
fn manifest_project_version_is_a_legacy_compatibility_field() {
let manifest = new_game_creation_app_manifest("project-1", "像素动作原型");
let payload = serde_json::to_value(&manifest).expect("manifest should serialize");
assert!(
payload.get("projectVersion").is_none(),
"原生不再写入版本标签,新清单不带这个键"
);
let mut legacy = payload.clone();
legacy["projectVersion"] = json!(6);
let parsed: GameCreationAppManifest =
serde_json::from_value(legacy).expect("含遗留 projectVersion 的存量清单必须仍可解析");
assert_eq!(parsed.project_version, Some(6));
assert!(
parsed.publication.is_none(),
"遗留键与发布绑定互相独立,不会凭它推导任何东西"
);
let mut published = parsed.clone();
published.publication = Some(GameCreationAppPublicationBinding {
account_id: "user-1".to_string(),
api_base_url: "https://dev.test".to_string(),
game_id: "game-1".to_string(),
revision: 3,
latest_version_id: Some("gamever-1".to_string()),
latest_version_number: Some(4),
status: Some("pending_review".to_string()),
});
let payload = serde_json::to_value(&published).expect("published manifest serializes");
assert_eq!(payload["projectVersion"], json!(6));
assert_eq!(payload["publication"]["latestVersionNumber"], json!(4));
assert_eq!(
serde_json::from_value::<GameCreationAppManifest>(payload)
.expect("legacy project version must round-trip")
.project_version,
Some(6)
);
}
/// 未知顶层字段必须失败关闭。这条契约级断言钉住的是本结构体本身的取向,而不是某个调用方:
/// AGC 客户端读写 manifest 的方式是「整结构体反序列化 + 整结构体重新序列化覆盖落盘」,
/// 一旦放行未知顶层字段,「读一次 + 任意一次写」就会静默抹掉未来版本新增的字段。
@@ -14,6 +14,14 @@ pub const GAME_DISTRIBUTION_CATEGORIES: [&str; 7] =
/// 客户端要么放行服务端必拒的包(白等一次上传),要么拦掉服务端能收的包。
pub const GAME_DISTRIBUTION_MAX_PACKAGE_BYTES: u64 = 200 * 1024 * 1024;
/// 版本号无法分配时的统一错误前缀(只在自动递增溢出时使用)。
///
/// 旧的「必须严格大于当前最大版本号」限制已经取消:同一 gameId 的同一个版本号可以反复提交,
/// 每次生成新的 versionId,也允许回退到更小的标签。显式版本号只要求 `>= 1`,`0` 这类非法值
/// 走普通 400;只有旧客户端缺省时按最大版本号自动递增、撞上 `u64::MAX` 才会带上此前缀,
/// api-server 据此映射成 HTTP 409。
pub const GAME_DISTRIBUTION_VERSION_NUMBER_CONFLICT: &str = "VERSION_NUMBER_CONFLICT";
/// 发布页免费生成简介与分类的输入。
///
/// 只传经过裁剪的项目摘要,不传本地绝对路径、聊天记录、凭据或完整 manifest。
@@ -224,6 +232,14 @@ pub struct GameDistributionCreateGameRequest {
pub struct GameDistributionCreateVersionRequest {
#[serde(default)]
pub local_project_id: Option<String>,
/// 用户可见的正整数版本标签(AGC 发布面板由工程内部版本序数派生后原样提交)。
///
/// 传入时只要求 `>= 1`:同一 gameId 的同一个版本号可以反复提交,每次提交生成新的
/// versionId,允许重复标签与回退到更小的版本号,不与已有最大值比较。缺省时保留旧客户端
/// 兼容行为:服务端按该游戏已有最大版本号 +1(此时溢出才会返回 409)。
/// 缺省时序列化省略该字段,保证旧客户端请求的幂等摘要与升级前一致。
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version_number: Option<u64>,
pub package_sha256: String,
pub package_bytes: u64,
pub package_file_count: u32,
@@ -231,6 +247,31 @@ pub struct GameDistributionCreateVersionRequest {
pub game_metadata: GameDistributionCreateGameRequest,
}
/// 作者编辑游戏级展示资料的请求体。
///
/// 只覆盖游戏行上的展示字段,随版本冻结的包摘要与资料快照不受影响;
/// `expected_publication_revision` 是公开切换 CAS,并发变化返回 409。
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionUpdateGameMetadataRequest {
pub expected_publication_revision: u64,
pub title: String,
pub summary: String,
#[serde(default)]
pub description: Option<String>,
pub category: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub cover_asset_id: Option<String>,
/// 截图素材 ID(最多 6 张,复用平台图片上传与归属校验)。
#[serde(default)]
pub screenshots: Vec<String>,
pub device_support: GameDistributionDeviceSupport,
pub input_modes: Vec<GameDistributionInputMode>,
pub orientation: GameDistributionOrientation,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameDistributionPrivateVersion {
@@ -239,11 +280,14 @@ pub struct GameDistributionPrivateVersion {
pub version_number: u64,
pub package_sha256: String,
pub package_bytes: u64,
pub package_file_count: u32,
pub status: GameDistributionVersionStatus,
/// 这一版所属游戏的公开切换版本号(撤回与审核回传用)。
pub publication_revision: u64,
#[serde(default)]
pub review_reason: Option<String>,
#[serde(default)]
pub entry_url: Option<String>,
pub created_at: String,
pub updated_at: String,
}
@@ -334,4 +378,68 @@ mod tests {
.expect("版本请求应可解析");
assert_eq!(payload.local_project_id.as_deref(), Some("proj-1"));
}
#[test]
fn create_version_request_version_number_is_optional() {
let legacy: GameDistributionCreateVersionRequest =
serde_json::from_value(serde_json::json!({
"packageSha256": "a".repeat(64),
"packageBytes": 10,
"packageFileCount": 1,
"packageEntryPath": "index.html",
"gameMetadata": {
"title": "游戏",
"summary": "简介",
"category": "益智",
"deviceSupport": { "desktop": true, "mobile": false, "touch": false },
"inputModes": ["keyboard"],
"orientation": "responsive",
},
}))
.expect("旧客户端不传版本号应继续可解析");
assert_eq!(legacy.version_number, None);
let explicit: GameDistributionCreateVersionRequest =
serde_json::from_value(serde_json::json!({
"versionNumber": 5,
"packageSha256": "a".repeat(64),
"packageBytes": 10,
"packageFileCount": 1,
"packageEntryPath": "index.html",
"gameMetadata": {
"title": "游戏",
"summary": "简介",
"category": "益智",
"deviceSupport": { "desktop": true, "mobile": false, "touch": false },
"inputModes": ["keyboard"],
"orientation": "responsive",
},
}))
.expect("显式版本号应可解析");
assert_eq!(explicit.version_number, Some(5));
let serialized = serde_json::to_value(&explicit).expect("显式版本号应可序列化");
assert_eq!(serialized["versionNumber"], 5);
}
#[test]
fn update_game_metadata_request_round_trips_publication_revision() {
let payload: GameDistributionUpdateGameMetadataRequest =
serde_json::from_value(serde_json::json!({
"expectedPublicationRevision": 3,
"title": "游戏",
"summary": "简介",
"category": "益智",
"coverAssetId": "asset-cover",
"screenshots": ["asset-shot"],
"deviceSupport": { "desktop": true, "mobile": false, "touch": false },
"inputModes": ["keyboard"],
"orientation": "responsive",
}))
.expect("资料编辑请求应可解析");
assert_eq!(payload.expected_publication_revision, 3);
assert_eq!(payload.cover_asset_id.as_deref(), Some("asset-cover"));
let serialized = serde_json::to_value(&payload).expect("资料编辑请求应可序列化");
assert_eq!(serialized["expectedPublicationRevision"], 3);
assert_eq!(serialized["coverAssetId"], "asset-cover");
}
}
@@ -25,13 +25,14 @@ pub use game_distribution::{
GameDistributionAdminGameListRecordInput, GameDistributionAdminUserReviewListRecordInput,
GameDistributionApproveRecordInput, GameDistributionCancelVersionRecordInput,
GameDistributionConfirmPackageRecordInput, GameDistributionCreateGameRecordInput,
GameDistributionCreateVersionRecordInput, GameDistributionFailUploadRecordInput,
GameDistributionGetGameRecordInput, GameDistributionOwnerGameListRecordInput,
GameDistributionPlayCountIncrementRecordInput, GameDistributionPublicGameListRecordInput,
GameDistributionRejectRecordInput, GameDistributionRestoreRecordInput,
GameDistributionReviewGameListRecordInput, GameDistributionReviewModerationRecordInput,
GameDistributionSubmitReviewRecordInput, GameDistributionSuspendRecordInput,
GameDistributionUnpublishRecordInput,
GameDistributionCreateVersionRecordInput, GameDistributionDeleteGameRecordInput,
GameDistributionFailUploadRecordInput, GameDistributionGetGameRecordInput,
GameDistributionOwnerGameListRecordInput, GameDistributionPlayCountIncrementRecordInput,
GameDistributionPublicGameListRecordInput, GameDistributionRejectRecordInput,
GameDistributionRestoreRecordInput, GameDistributionReviewGameListRecordInput,
GameDistributionReviewModerationRecordInput, GameDistributionSubmitReviewRecordInput,
GameDistributionSuspendRecordInput, GameDistributionUnpublishRecordInput,
GameDistributionUpdateMetadataRecordInput,
};
#[path = "active/external_generation.rs"]
pub mod external_generation;
@@ -179,6 +179,7 @@ pub struct GameDistributionGameRecord {
pub created_at: String,
pub updated_at: String,
pub cover_object_key: Option<String>,
pub local_project_id: Option<String>,
pub screenshots_json: Option<String>,
}
@@ -232,6 +233,8 @@ pub struct GameDistributionAdminGameRecord {
pub created_at: String,
pub updated_at: String,
pub versions: Vec<GameDistributionAdminVersionRecord>,
/// 软删除时间;为空表示未删除。
pub deleted_at: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -272,6 +275,7 @@ fn map_game(
play_count: value.play_count,
created_at: shared_kernel::format_timestamp_micros(value.created_at_micros),
updated_at: shared_kernel::format_timestamp_micros(value.updated_at_micros),
local_project_id: value.local_project_id,
cover_object_key: value.cover_object_key,
screenshots_json: value.screenshots_json,
}
@@ -338,16 +342,22 @@ fn map_admin_game(
created_at: shared_kernel::format_timestamp_micros(value.created_at_micros),
updated_at: shared_kernel::format_timestamp_micros(value.updated_at_micros),
versions: value.versions.into_iter().map(map_admin_version).collect(),
deleted_at: value
.deleted_at_micros
.map(shared_kernel::format_timestamp_micros),
}
}
pub(crate) fn map_game_distribution_admin_game_list_result(
result: crate::module_bindings::GameDistributionAdminGameListResult,
) -> Result<Vec<GameDistributionAdminGameRecord>, SpacetimeClientError> {
) -> Result<(Vec<GameDistributionAdminGameRecord>, Option<String>), SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
Ok(result.games.into_iter().map(map_admin_game).collect())
Ok((
result.games.into_iter().map(map_admin_game).collect(),
result.next_cursor,
))
}
pub(crate) fn map_game_distribution_game_result(
@@ -37,6 +37,14 @@ pub struct GameDistributionPublicGameListRecordInput {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GameDistributionAdminGameListRecordInput {
pub limit: u32,
/// 关键词:匹配标题、gameId 或作者 user ID。
pub keyword: Option<String>,
/// 只返回该作者名下的游戏。
pub owner_user_id: Option<String>,
/// `published` / `unpublished` / `suspended` / `deleted`;为空时排除已软删除游戏。
pub status: Option<String>,
/// 上一页返回的游标。
pub cursor: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -87,6 +95,9 @@ pub struct GameDistributionCreateVersionRecordInput {
pub game_id: String,
pub owner_user_id: String,
pub version_id: String,
/// 用户可见的正整数版本标签;`None` 时由服务端按已有最大版本号自动 +1(旧客户端兼容)。
/// 传入时只要求 `>= 1`:同一 gameId 的同一标签可以反复提交,每次生成新的 version_id。
pub version_number: Option<u64>,
/// api-server 校验并解析素材后生成的资料快照 JSON。
pub metadata_json: String,
pub package_sha256: String,
@@ -198,6 +209,41 @@ pub struct GameDistributionSuspendRecordInput {
pub now_micros: i64,
}
/// 作者编辑游戏级展示资料;对象键由 api-server 从素材记录派生后传入。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GameDistributionUpdateMetadataRecordInput {
pub game_id: String,
pub owner_user_id: String,
pub expected_publication_revision: u64,
pub title: String,
pub summary: String,
pub description: Option<String>,
pub category: String,
pub tags_json: String,
pub cover_asset_id: Option<String>,
pub cover_object_key: Option<String>,
pub screenshots_json: Option<String>,
pub device_support_desktop: bool,
pub device_support_mobile: bool,
pub device_support_touch: bool,
pub input_modes_json: String,
pub orientation: String,
pub idempotency_key: String,
pub request_digest: String,
pub now_micros: i64,
}
/// 作者软删除游戏;只标记删除时间并下线公开投影。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GameDistributionDeleteGameRecordInput {
pub game_id: String,
pub owner_user_id: String,
pub expected_publication_revision: u64,
pub idempotency_key: String,
pub request_digest: String,
pub now_micros: i64,
}
impl SpacetimeClient {
pub async fn list_admin_game_distribution_user_reviews(
&self,
@@ -664,6 +710,7 @@ impl SpacetimeClient {
game_id: input.game_id,
owner_user_id: input.owner_user_id,
version_id: input.version_id,
version_number: input.version_number,
metadata_json: input.metadata_json,
package_sha_256: input.package_sha256,
package_bytes: input.package_bytes,
@@ -1035,13 +1082,18 @@ impl SpacetimeClient {
.await
}
/// 后台游戏管理页的全量游戏列表:含版本数与最近版本历史,作者名/头像由事务内读时联。
/// 后台游戏管理页列表:关键词/作者/状态过滤 + 游标分页,作者名/头像由事务内读时联。
pub async fn list_admin_game_distribution_games(
&self,
input: GameDistributionAdminGameListRecordInput,
) -> Result<Vec<GameDistributionAdminGameRecord>, SpacetimeClientError> {
let procedure_input =
crate::module_bindings::GameDistributionAdminGameListInput { limit: input.limit };
) -> Result<(Vec<GameDistributionAdminGameRecord>, Option<String>), SpacetimeClientError> {
let procedure_input = crate::module_bindings::GameDistributionAdminGameListInput {
limit: input.limit,
keyword: input.keyword,
owner_user_id: input.owner_user_id,
status: input.status,
cursor: input.cursor,
};
self.call_after_connect(
"list_admin_game_distribution_games",
move |connection, sender| {
@@ -1061,6 +1113,93 @@ impl SpacetimeClient {
.await
}
/// 作者编辑游戏级展示资料;版本包与冻结资料不受影响。
pub async fn update_game_distribution_game_metadata(
&self,
input: GameDistributionUpdateMetadataRecordInput,
) -> Result<(GameDistributionGameRecord, bool), SpacetimeClientError> {
let procedure_input = crate::module_bindings::GameDistributionUpdateMetadataInput {
game_id: input.game_id,
owner_user_id: input.owner_user_id,
expected_publication_revision: input.expected_publication_revision,
title: input.title,
summary: input.summary,
description: input.description,
category: input.category,
tags_json: input.tags_json,
cover_asset_id: input.cover_asset_id,
cover_object_key: input.cover_object_key,
screenshots_json: input.screenshots_json,
device_support_desktop: input.device_support_desktop,
device_support_mobile: input.device_support_mobile,
device_support_touch: input.device_support_touch,
input_modes_json: input.input_modes_json,
orientation: input.orientation,
idempotency_key: input.idempotency_key,
request_digest: input.request_digest,
now_micros: input.now_micros,
};
self.call_after_connect(
"update_game_distribution_game_metadata",
move |connection, sender| {
connection
.procedures()
.update_game_distribution_game_metadata_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_game_distribution_game_result)
.and_then(|(game, _, replayed)| {
game.map(|game| (game, replayed)).ok_or_else(|| {
SpacetimeClientError::missing_snapshot("游戏资料更新结果")
})
});
send_once(&sender, mapped);
},
);
},
)
.await
}
/// 作者软删除游戏;只下线公开投影,版本行与冻结资料保留。
pub async fn delete_game_distribution_game(
&self,
input: GameDistributionDeleteGameRecordInput,
) -> Result<(GameDistributionGameRecord, bool), SpacetimeClientError> {
let procedure_input = crate::module_bindings::GameDistributionDeleteGameInput {
game_id: input.game_id,
owner_user_id: input.owner_user_id,
expected_publication_revision: input.expected_publication_revision,
idempotency_key: input.idempotency_key,
request_digest: input.request_digest,
now_micros: input.now_micros,
};
self.call_after_connect(
"delete_game_distribution_game",
move |connection, sender| {
connection
.procedures()
.delete_game_distribution_game_and_return_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(map_game_distribution_game_result)
.and_then(|(game, _, replayed)| {
game.map(|game| (game, replayed)).ok_or_else(|| {
SpacetimeClientError::missing_snapshot("游戏删除结果")
})
});
send_once(&sender, mapped);
},
);
},
)
.await
}
/// 管理员解除安全下架:重新激活最近一次曾公开的版本。
pub async fn restore_game_distribution_game(
&self,
@@ -202,6 +202,7 @@ pub mod delete_editor_asset_and_return_procedure;
pub mod delete_editor_asset_folder_and_return_procedure;
pub mod delete_editor_project_and_return_procedure;
pub mod delete_error_report_and_return_procedure;
pub mod delete_game_distribution_game_and_return_procedure;
pub mod editor_agent_conversation_create_input_type;
pub mod editor_agent_conversation_delete_input_type;
pub mod editor_agent_conversation_get_input_type;
@@ -421,6 +422,7 @@ pub mod game_distribution_cancel_version_input_type;
pub mod game_distribution_confirm_package_input_type;
pub mod game_distribution_create_game_input_type;
pub mod game_distribution_create_version_input_type;
pub mod game_distribution_delete_game_input_type;
pub mod game_distribution_fail_upload_input_type;
pub mod game_distribution_game_list_procedure_result_type;
pub mod game_distribution_game_procedure_result_type;
@@ -455,6 +457,7 @@ pub mod game_distribution_review_type;
pub mod game_distribution_submit_review_input_type;
pub mod game_distribution_suspend_input_type;
pub mod game_distribution_unpublish_input_type;
pub mod game_distribution_update_metadata_input_type;
pub mod game_distribution_user_review_input_type;
pub mod game_distribution_user_review_list_input_type;
pub mod game_distribution_user_review_result_type;
@@ -875,6 +878,7 @@ pub mod update_editor_project_resource_showcase_and_return_procedure;
pub mod update_editor_showcase_asset_display_and_return_procedure;
pub mod update_error_report_and_return_procedure;
pub mod update_external_generation_job_phase_and_return_procedure;
pub mod update_game_distribution_game_metadata_and_return_procedure;
pub mod update_payment_app_and_return_procedure;
pub mod upload_agc_analytics_batch_procedure;
pub mod upsert_editor_generation_pricing_config_and_return_procedure;
@@ -1084,6 +1088,7 @@ pub use delete_editor_asset_and_return_procedure::delete_editor_asset_and_return
pub use delete_editor_asset_folder_and_return_procedure::delete_editor_asset_folder_and_return;
pub use delete_editor_project_and_return_procedure::delete_editor_project_and_return;
pub use delete_error_report_and_return_procedure::delete_error_report_and_return;
pub use delete_game_distribution_game_and_return_procedure::delete_game_distribution_game_and_return;
pub use editor_agent_conversation_create_input_type::EditorAgentConversationCreateInput;
pub use editor_agent_conversation_delete_input_type::EditorAgentConversationDeleteInput;
pub use editor_agent_conversation_get_input_type::EditorAgentConversationGetInput;
@@ -1303,6 +1308,7 @@ pub use game_distribution_cancel_version_input_type::GameDistributionCancelVersi
pub use game_distribution_confirm_package_input_type::GameDistributionConfirmPackageInput;
pub use game_distribution_create_game_input_type::GameDistributionCreateGameInput;
pub use game_distribution_create_version_input_type::GameDistributionCreateVersionInput;
pub use game_distribution_delete_game_input_type::GameDistributionDeleteGameInput;
pub use game_distribution_fail_upload_input_type::GameDistributionFailUploadInput;
pub use game_distribution_game_list_procedure_result_type::GameDistributionGameListProcedureResult;
pub use game_distribution_game_procedure_result_type::GameDistributionGameProcedureResult;
@@ -1337,6 +1343,7 @@ pub use game_distribution_review_type::GameDistributionReview;
pub use game_distribution_submit_review_input_type::GameDistributionSubmitReviewInput;
pub use game_distribution_suspend_input_type::GameDistributionSuspendInput;
pub use game_distribution_unpublish_input_type::GameDistributionUnpublishInput;
pub use game_distribution_update_metadata_input_type::GameDistributionUpdateMetadataInput;
pub use game_distribution_user_review_input_type::GameDistributionUserReviewInput;
pub use game_distribution_user_review_list_input_type::GameDistributionUserReviewListInput;
pub use game_distribution_user_review_result_type::GameDistributionUserReviewResult;
@@ -1757,6 +1764,7 @@ pub use update_editor_project_resource_showcase_and_return_procedure::update_edi
pub use update_editor_showcase_asset_display_and_return_procedure::update_editor_showcase_asset_display_and_return;
pub use update_error_report_and_return_procedure::update_error_report_and_return;
pub use update_external_generation_job_phase_and_return_procedure::update_external_generation_job_phase_and_return;
pub use update_game_distribution_game_metadata_and_return_procedure::update_game_distribution_game_metadata_and_return;
pub use update_payment_app_and_return_procedure::update_payment_app_and_return;
pub use upload_agc_analytics_batch_procedure::upload_agc_analytics_batch;
pub use upsert_editor_generation_pricing_config_and_return_procedure::upsert_editor_generation_pricing_config_and_return;
@@ -31,10 +31,10 @@ pub trait approve_game_distribution_version_and_return {
input: GameDistributionApproveInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl approve_game_distribution_version_and_return for super::RemoteProcedures {
input: GameDistributionApproveInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -34,10 +34,10 @@ pub trait cancel_game_distribution_version_and_return {
input: GameDistributionCancelVersionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl cancel_game_distribution_version_and_return for super::RemoteProcedures {
input: GameDistributionCancelVersionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -34,10 +34,10 @@ pub trait confirm_game_distribution_package_and_return {
input: GameDistributionConfirmPackageInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl confirm_game_distribution_package_and_return for super::RemoteProcedures {
input: GameDistributionConfirmPackageInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -31,10 +31,10 @@ pub trait create_game_distribution_game_and_return {
input: GameDistributionCreateGameInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl create_game_distribution_game_and_return for super::RemoteProcedures {
input: GameDistributionCreateGameInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -34,10 +34,10 @@ pub trait create_game_distribution_version_and_return {
input: GameDistributionCreateVersionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -47,10 +47,10 @@ impl create_game_distribution_version_and_return for super::RemoteProcedures {
input: GameDistributionCreateVersionInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -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::game_distribution_delete_game_input_type::GameDistributionDeleteGameInput;
use super::game_distribution_procedure_result_type::GameDistributionProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct DeleteGameDistributionGameAndReturnArgs {
pub input: GameDistributionDeleteGameInput,
}
impl __sdk::InModule for DeleteGameDistributionGameAndReturnArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `delete_game_distribution_game_and_return`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait delete_game_distribution_game_and_return {
fn delete_game_distribution_game_and_return(&self, input: GameDistributionDeleteGameInput) {
self.delete_game_distribution_game_and_return_then(input, |_, _| {});
}
fn delete_game_distribution_game_and_return_then(
&self,
input: GameDistributionDeleteGameInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl delete_game_distribution_game_and_return for super::RemoteProcedures {
fn delete_game_distribution_game_and_return_then(
&self,
input: GameDistributionDeleteGameInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
"delete_game_distribution_game_and_return",
DeleteGameDistributionGameAndReturnArgs { input },
__callback,
);
}
}
@@ -31,10 +31,10 @@ pub trait fail_game_distribution_upload_and_return {
input: GameDistributionFailUploadInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -44,10 +44,10 @@ impl fail_game_distribution_upload_and_return for super::RemoteProcedures {
input: GameDistributionFailUploadInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
&super::ProcedureEventContext,
Result<GameDistributionProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, GameDistributionProcedureResult>(
@@ -8,6 +8,10 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[sats(crate = __lib)]
pub struct GameDistributionAdminGameListInput {
pub limit: u32,
pub keyword: Option<String>,
pub owner_user_id: Option<String>,
pub status: Option<String>,
pub cursor: Option<String>,
}
impl __sdk::InModule for GameDistributionAdminGameListInput {
@@ -12,6 +12,7 @@ pub struct GameDistributionAdminGameListResult {
pub ok: bool,
pub games: Vec<GameDistributionAdminGameSnapshot>,
pub error_message: Option<String>,
pub next_cursor: Option<String>,
}
impl __sdk::InModule for GameDistributionAdminGameListResult {

Some files were not shown because too many files have changed in this diff Show More