契约先行(sdk-interface.d.ts+镜像 additive):白名单新增恰等键族 save:{gameId}:{versionId}(≤65536 码元,
写入必须 {schemaVersion≥1, data} 信封,宿主闸 fail-closed);idle: 族 4096 逐字不动;弃多 key 分片。
宿主闸抽纯函数模块 storageGate.ts(四闸合一可 node 直测);save-progress 插件 v1.0.0→v1.1.0 additive:
setVersioned/getVersioned 逐级迁移/缺级兜底/降档读新档不毁档/无版本旧档按 0 版进链+探针两计数。
证据:插件单测 18/18、game-runtime 31/31、宿主闸负向演示脚本入仓真跑全断言过、vue-tsc+vite build 绿。
三书 M 门影子存档降级口径解除条件就位(卡牌 M2/割草 M4/夜市 M5 切换)。设计页 docs/plans/2026-07-06-002。
153 lines
7.8 KiB
TypeScript
153 lines
7.8 KiB
TypeScript
/**
|
||
* 契约 #3 | WanxiangGameSDK 公共 API 类型 + SDK↔宿主 postMessage 协议
|
||
* owner:WS3 SDK 负责人 | 消费方:game-studio 宿主 / 游戏侧
|
||
*
|
||
* 硬约束(来自 engineering-conventions §3):
|
||
* - 零第三方依赖;Core(Lifecycle+EventBus+Telemetry+ErrorTrack) 压缩后 < 8KB;每 Plugin < 5KB
|
||
* - 所有对外 API 不返回 Promise(fire-and-forget),不占游戏主线程,异常绝不向游戏抛
|
||
* - 新版本只增字段/方法、不删不改已有签名(semver)
|
||
*/
|
||
|
||
/** SDK 初始化参数(宿主注入) */
|
||
export interface WanxiangGameSDKInitOptions {
|
||
gameId: string;
|
||
versionId: string;
|
||
/** 贯穿 生成→编译→加载→运行→上报 的 trace_id */
|
||
traceId: string;
|
||
/** 是否开发模式(加载 Plugin.Debug,仅 dev) */
|
||
debug?: boolean;
|
||
}
|
||
|
||
/** 游戏生命周期事件名 */
|
||
export type LifecycleEvent =
|
||
| 'sdk_ready' // SDK 初始化完成
|
||
| 'game_loaded' // 游戏资源加载完成
|
||
| 'game_start' // 开始游玩
|
||
| 'game_end' // 本局结束
|
||
| 'game_error'; // 运行错误
|
||
|
||
/** SDK 根对象(挂在游戏侧 window.WanxiangGameSDK) */
|
||
export interface WanxiangGameSDK {
|
||
/** 初始化(宿主调用一次) */
|
||
init(options: WanxiangGameSDKInitOptions): void;
|
||
/** 生命周期事件订阅(EventBus) */
|
||
on(event: LifecycleEvent, handler: (payload?: Record<string, unknown>) => void): void;
|
||
off(event: LifecycleEvent, handler: (payload?: Record<string, unknown>) => void): void;
|
||
/** 遥测上报(fire-and-forget,内部 10条/5s 批量 flush + sendBeacon 兜底) */
|
||
track(event: string, props?: Record<string, unknown>): void;
|
||
/** 错误上报(去重,不中断游戏) */
|
||
reportError(error: { message: string; stack?: string }): void;
|
||
/** 插件(首次调用懒加载) */
|
||
ad: AdPlugin;
|
||
pay: PayPlugin;
|
||
/** 存储插件(首次调用懒加载;idle 离线存档;T1b-β 由前端 host/contract.ts 定稿形态上提至根契约) */
|
||
storage: StoragePlugin;
|
||
}
|
||
|
||
/** 广告插件(MVP 桩 → 真实穿山甲/优量汇切换;每调用 5s 超时降级) */
|
||
export interface AdPlugin {
|
||
/** 激励视频;完成回调 rewarded=true */
|
||
showRewarded(slotId: string, cb: (result: { rewarded: boolean }) => void): void;
|
||
/** 插屏广告 */
|
||
showInterstitial(slotId: string, cb?: (result: { shown: boolean }) => void): void;
|
||
}
|
||
|
||
/** 支付插件(MVP 桩 → 真实微信支付切换) */
|
||
export interface PayPlugin {
|
||
/** 发起积分充值;宿主侧拉起支付 */
|
||
pay(order: { orderId: string; amount: number }, cb: (result: { paid: boolean }) => void): void;
|
||
}
|
||
|
||
/** 存储插件(idle 离线产出存档;HJ-MC-TPL-EXEC-002 §6.1.1 定稿对象形态,T1b-β 上提至根契约,semver 只增不改) */
|
||
export interface StoragePlugin {
|
||
/**
|
||
* 写入存档(fire-and-forget,不返回 Promise)。宿主侧白名单前缀校验 + 大小上限校验后落 localStorage。
|
||
* @param key 存档键(runtime 侧按 'idle:'+gameId+':'+versionId 约定)。
|
||
* @param value 存档值(JSON 字符串;宿主侧落库前校验大小)。
|
||
*/
|
||
save(key: string, value: string): void;
|
||
/**
|
||
* 读取存档(宿主受信边界已解析为对象后回调;runtime 侧零 JSON.parse,守 runtime §5.5 零解析红线)。
|
||
* @param key 存档键。
|
||
* @param cb 回调;命中=已解析对象,未命中/校验失败/**非纯对象(含数组)**=null(见 StorageResultPayload.value 的 array 排除)。
|
||
*/
|
||
load(key: string, cb: (value: Record<string, unknown> | null) => void): void;
|
||
}
|
||
|
||
/* ============================================================================
|
||
* SDK ↔ 宿主 postMessage 协议
|
||
* 游戏在 iframe 沙箱内,通过 postMessage 与 game-studio 宿主通信。
|
||
* 宿主侧校验 origin 白名单 + 消息 schema(双校验,见 security-and-reliability §1.1)。
|
||
* ========================================================================== */
|
||
|
||
/** 消息方向 */
|
||
export type MessageDirection = 'game_to_host' | 'host_to_game';
|
||
|
||
/** 消息类型全集 */
|
||
export type PostMessageType =
|
||
| 'init' // 宿主→游戏:初始化参数
|
||
| 'lifecycle' // 游戏→宿主:生命周期事件
|
||
| 'telemetry' // 游戏→宿主:遥测事件转发
|
||
| 'error' // 游戏→宿主:错误上报
|
||
| 'ad' // 双向:广告请求/结果(广告在 iframe 外宿主侧渲染)
|
||
| 'pay' // 双向:支付请求/结果
|
||
| 'social' // 双向:分享等
|
||
| 'storage'; // 双向:键值存储
|
||
|
||
/** postMessage 信封 */
|
||
export interface PostMessageEnvelope<T = unknown> {
|
||
/** 固定标识,宿主据此过滤非本协议消息 */
|
||
channel: 'wanxiang-game-sdk';
|
||
type: PostMessageType;
|
||
direction: MessageDirection;
|
||
/** 请求/响应配对 ID(ad/pay 等需回调的消息) */
|
||
requestId?: string;
|
||
traceId: string;
|
||
payload: T;
|
||
}
|
||
|
||
/* ----- storage 消息 payload(idle 离线产出存档;HJ-MC-TPL-EXEC-002 §6.1.1,T1b-β 上提,semver 只增不改) ----- */
|
||
|
||
/**
|
||
* 存档键族与分层上限(W-SAVE-CAP 2026-07-06 增补,additive)。宿主白名单恰等匹配、每族一槽:
|
||
* - `idle:{gameId}:{versionId}`:轻量离线产出存档。上限 4096 码元(value.length),无信封要求——存量语义逐字不变。
|
||
* - `save:{gameId}:{versionId}`:复杂游戏主存档(北极星三款起用)。上限 65536 码元,写入值**必须**是
|
||
* SaveEnvelope 版本信封的 JSON 序列化,宿主闸 fail-closed(非信封/版本非法一律丢弃)。
|
||
* 两族之外的 key 一律丢弃。上限口径 = UTF-16 码元数(与宿主 localStorage 存储口径一致);
|
||
* 游戏侧 save-progress 插件的 maxValueBytes 是 UTF-8 字节口径的更紧缺省内闸(32KB,可上调)。
|
||
*/
|
||
export interface SaveEnvelope {
|
||
/** 存档 schema 版本(整数 ≥1;无版本旧档由读取侧按 0 处理进迁移链——见 save-progress getVersioned) */
|
||
schemaVersion: number;
|
||
/** 存档正文(纯对象;数组/标量非法——与 StorageResultPayload 的「对象语义不含数组」一致) */
|
||
data: Record<string, unknown>;
|
||
}
|
||
|
||
/** storage 写入请求 payload(游戏→宿主:保存键值) */
|
||
export interface StorageSetPayload {
|
||
/** 存档键(键族与上限见 SaveEnvelope 注释:'idle:'|'save:' + gameId + ':' + versionId,宿主侧白名单恰等校验) */
|
||
key: string;
|
||
/** 存档值(JSON 字符串,宿主侧落 localStorage 前按键族校验大小上限;save: 族还须为 SaveEnvelope 信封) */
|
||
value: string;
|
||
}
|
||
|
||
/** storage 读取请求 payload(游戏→宿主:按 key 取值;带 requestId,宿主读分支按同 requestId 回包) */
|
||
export interface StorageGetPayload {
|
||
key: string;
|
||
}
|
||
|
||
/** storage 读取结果 payload(宿主→游戏:回包 value,无则 null) */
|
||
export interface StorageResultPayload {
|
||
key: string;
|
||
/**
|
||
* 命中的存档值 = 宿主受信边界已解析好的对象(非原始 JSON 字符串);未命中/校验失败为 null。
|
||
* 解析在宿主侧做:runtime 侧零 JSON.parse(守 runtime §5.5「runtime 内不引可抛解析路径」),
|
||
* loadState resolve 出对象后 runtime 直接读字段。写入 payload(StorageSetPayload.value)仍是 JSON 字符串。
|
||
* **对象语义不含数组(T1b-β R4-P1)**:宿主回包端(game-studio inject.ts 已落 !Array.isArray 运行时排除)
|
||
* 与本类型层须一致——value 虽 TS 标注 Record<string,unknown>|null(array 在 TS 是 object 子型不被拒),
|
||
* 但**契约语义明令排除数组**:数组/字符串/标量一律按未命中处理(resolve null)。
|
||
* storage 三套形状(本根契约 / game-studio host/contract.ts / inject.ts 应答端)收敛校验须覆盖此「array→null」运行时语义,不止类型签名对齐。
|
||
*/
|
||
value: Record<string, unknown> | null;
|
||
}
|