Caputchin
应用市场游戏开发

回放契约

回放契约是一个 可回放 游戏和平台约定的那一个强制面。它作为 @caputchin/replay-contract 发布:一个刻意极小、零依赖的包(它被加载进一个密封的回放隔离体,所以它什么都不拉)。关于你游戏的其他一切,引擎、渲染器、轨迹格式,都归你。

一个交付了一个合规 run 的游戏是 可回放 的,因而能给一个站点密钥 设关卡;一个没有它的游戏仍可嵌入,但只作为 UX(它显示 不可回放)。那个区分见 向应用市场交付一个游戏

run 函数

一个可回放游戏交付一个 JS 或 WASM 模块,导出一个名为 run 的函数:

run(seed, config, trace) -> { passed, score, durationMs }
type RunFn<C = unknown> = (
  seed: Seed,
  config: C | null,
  trace: Uint8Array | string,
) => Verdict | Promise<Verdict>
参数来源含义
seed服务器逐回合的 种子。这个回合服务器拥有的搭建。
config服务器玩法配置,可为空且不透明:平台从不检视它,而且它在回放时于服务端重新解析,绝非客户端声称。null 意味着“用 run 自己的默认”。在 MVP 阶段服务器传 null;按站点配置是一个延后的阶段。
trace客户端玩家客户端发出的那团不透明 blob,只由这个函数解读。平台从不解析或为它定类型;它是原始字节或一个字符串,只受一个大小上限和隔离体 CPU 限制约束。

参数顺序编码了那个信任模型:seedconfig 是这个回合的服务器搭建;trace 是玩家的输入。影响关卡的值(一个通过阈值、生命数)从 config 读是安全的,但若从客户端 trace 读则会是一个绕过。

模块必须以名字 run 导出这个函数(常量 RUN_EXPORT_NAME)。回放宿主调用它并等待结果(WASM 实例化在首次调用时是异步的)。

那个种子

type Seed = readonly [number, number, number, number]   // 128 bits, 4 u32 words, MSW first

种子是 SHA-256(sessionId : gameId : roundIndex) 的低 128 位,作为四个无符号 32 位字携带,最高有效字在前。这个包的 deriveSeed(sessionId, gameId, roundIndex) 算出它;服务器在签发这个回合时和重新回放时都派生它,所以它绝不作为被信任的客户端输入跑在网线上。

至关重要的是,种子 把一条轨迹绑定到一个会话、游戏和回合:在一个不同的种子下回放一条外来的或更早的轨迹会得到 passed: false。那个绑定就是轨迹注入和回放攻击被防御的方式,所以你必须从它给所有随机性播种,好让实时游玩和服务器回放一致。

那个裁定

interface Verdict {
  readonly passed: boolean      // drives the verification decision
  readonly score: number        // game-defined, any finite number
  readonly durationMs: number   // finite, non-negative
}

passed 是那唯一驱动 captcha 决定的字段;scoredurationMs 装在签发的令牌里(以及一个未来的记分牌)。这个包的 parseVerdict(value) 校验一个不被信任的返回值,并 把一个畸形的结果当作一个被拒绝的回合,绝不当作一个通过的,所以一个抛出或返回垃圾的 run 失败关闭。

确定性就是全部的要求

这个契约那一条硬性规则:run 必须 跨两个运行时纯且确定性,即玩家的浏览器和服务器隔离体。相同的 (seed, config, trace) 必须在两者里都得到一个相同的裁定。打破这个的常见方式,全都被发布自检逮成 run-not-conforming

  • Date.now()performance.now()Math.random() 或其他非确定性的全局量;
  • 读回放隔离体不提供的外部状态(DOM、网络、存储);
  • 倚靠在不同运行时之间有差异的浮点数学。

你如何达成确定性(定点、WASM 规范浮点,或 IEEE-754 加一个中和垫片)由你选;平台只承载这个回放。如果你宁愿不自己造,那个 引擎套件 提供现成的确定性原语。

服务器如何使用它

隔离体加载的那个工件是你的 run 包,要么是实时的 entry,要么是清单里那个专用的 run 工件

另见

本页内容