npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@1game/cli-1gameplay

v1.21.0

Published

1Game gameplay recording CLI

Readme

@1game/cli-1gameplay(1gameplay)

@1game/cli-1gameplay 是 1Game 的无头录制与回放工具包,命令名为 1gameplay

日常入口是 1gameplay 命令。本包同时导出给 @1game/test 使用的 JS API(addFrameComments / listFrameComments / bundleReplayHtml 等);不承诺其它内部模块路径稳定。

本 README 面向使用者,说明如何构建、如何执行常用命令、以及常见错误处理方式。

快速开始

在仓库根目录安装依赖:

  • pnpm install

构建 CLI 包:

  • pnpm run build(工作目录:packages-public/cli-1gameplay

查看帮助:

  • node ./bin/1gameplay.mjs --help(工作目录:packages-public/cli-1gameplay

说明:命令入口位于 bin/1gameplay.mjs,会调用 dist/cli.mjs 导出的 runCli

归档格式说明(v2)

  • 唯一形态:单个 SQLite 文件,扩展名为 .1gamerecord(例如 out/demo.1gamerecord)。
  • 存储:符合 SQLite Archive (sqlar)sqlar 表存放 bundle/game.worker.jsbundle/worker-bootstrap.js;结构化表 record_metaframes(DAG)、record_branches(refs)、eventsstore_rowskv_store / kv_store_journalruntime_logs(可选,step --capture-console 采集的 console 调试元数据)、runtime_perf(可选,step --perf summary 写一行 1gameplay.perf.summary/1frames|trace 写逐帧 1gameplay.perf.frame/1)等存放录制与元数据(不再使用目录 + ndjson 主存储)。Schema 版本见 GAMEPLAY_SCHEMA_VERSION
  • 破坏性变更:v2 不再支持旧「目录归档」;传入目录会得到 CLI_BAD_ARGS(见错误提示)。不提供自动迁移命令。
  • frame query|diff|batch 使用 frame protocol:stdout 固定为 JSON envelope(成功 1gameplay.frame,失败 1gameplay.error),并按错误类型映射退出码(见下文)。frame screenshot 成功为 1gameplay.screenshot.v1frame paint 成功为 1gameplay.paint.snapshot/1

1game init 默认约定的配合:1game init 生成的项目将构建输出目录设为 out/,并在 .gitignore 中忽略 out/*.1gamerecord*.1gamerecord.swp。下文示例将录制文件与回放 HTML 也放在 out/ 下,便于与浏览器构建产物一起被忽略、避免误提交。

--dry-run / .swp 试跑会话

用途:在不改主档的前提下做完整 step 试跑(可多帧追加);其它命令加 --dry-run 时读写 sidecar <archive>.swp

规则:

  • --dry-run → 只碰主档;主档读取不受 .swp 影响
  • --dry-run → 只碰 .swp
  • step --dry-run 会在 .swp 缺失时从主档复制创建;其它命令要求 .swp 已存在
  • 存在 .swp 时,无 --dry-run 的主档写入硬失败(含 create --out 同一路径;create 本身不支持 --dry-run
  • 不提供 commit/abort 子命令:采纳 mv <archive>.swp <archive>,丢弃 rm <archive>.swp

step --dry-run 会把真实 step 提交进 sidecar(可 --from-frame 截断暂存时间线),不是进程内假 tick。观察用 frame query --dry-run / frame paint --dry-run

示例:

1gameplay step out/demo.1gamerecord --dry-run --ms 16 --event '...'
1gameplay frame query out/demo.1gamerecord --dry-run --at last --select store:state
1gameplay step out/demo.1gamerecord --dry-run --ms 16 --repeat 2
1gameplay step out/demo.1gamerecord --ms 16   # 有 .swp 时失败
mv out/demo.1gamerecord.swp out/demo.1gamerecord

命令总览

1gameplay create --entry <path> --out <file.1gamerecord>
1gameplay branch create <file.1gamerecord> [--dry-run] --name <语义化分支名> [--at <seq|last>] [--from-branch <id>]
1gameplay step <file.1gamerecord> [--dry-run] [--ms <n>=100] [--repeat <n>] [--until <pred>] [--until-not] [--until-ms <n>] [--event <json>] [--event-file <path>] [--from-frame <seq>] [--branch <id>] [--perf off|summary|frames|trace] [--perf-max-frames <n>] [--surface record|display] [--flush] [--capture-console off|error|warn|info|log|debug|all] [--console-max-per-step <n>] [--console-max-bytes <n>]
1gameplay obs report <file.1gamerecord> [--profile lab.worker|lab.display|lab.paint|device.budget] [--at last|spike|<seq>] [--out report.json]
1gameplay comments add <file.1gamerecord> [--dry-run] --at <seq|last> --body <text> [--author-id <id>] [--anchor-json <json>] [--comment-id <id>]
1gameplay frames list <file.1gamerecord> [--dry-run] [--from <n>=0] [--to <n>=0]
1gameplay frame query <file.1gamerecord> [--dry-run] --at <seq|last> --select <expr>... [--payload summary|full]
1gameplay frame diff <file.1gamerecord> [--dry-run] --from <seq> --to <seq|last> --select <expr>... [--payload summary|full]
1gameplay frame batch <file.1gamerecord> [--dry-run] --plan-file <path> [--payload summary|full]
1gameplay frame screenshot <file.1gamerecord> [--dry-run] --at <seq|last> --out <file.png|jpg> [--scene-stable-uid <stableUid>] [--width <n>] [--height <n>] [--dpr <n>] [--format png|jpeg] [--quality <1-100>]
1gameplay frame paint <file.1gamerecord> [--dry-run] --at <seq|last> [--scene-stable-uid <stableUid>] [--width <n>] [--height <n>] [--dpr <n>] [--top <0-20>]
1gameplay bundle-player-html <file.1gamerecord> [--dry-run] --out <replay.html> [--title <s>] [--single-html|--multi-file]
1gameplay frames delete <file.1gamerecord> [--dry-run] (--ranges <spec> | --last <n>) [--compact]
1gameplay record extract <file.1gamerecord> [--dry-run] --out <file.1gamerecord> (--from-seq <n> --to-seq <n> | --from-ms <n> --to-ms <n>) [--merge-diff [n]] [--branch-meta]

bundle-player-html 默认单文件会把归档 gzip 后再 base64 写入 recordBase64(并带 recordEncoding: "gzip")。

使用说明

1) 创建归档:create

用途:通过 1game build --target worker --with-record 将入口构建成 game.worker.js(仅用户代码 + virtual solid re-export shim;@1game/engine-bundle/runtime/* 通过 externalization 在运行时按 ./engine/*.mjs 或 CDN URL 加载,详见 docs/engine-cdn-resolution.md),并初始化 .1gamerecord 文件(record_meta.bundleFormat 恒为 buildjs-worker,bundle 写入 sqlar)。本地模式下引擎运行时文件被同步捕获到 bundle/engine/*;CDN 模式(ONEGAME_ENGINE_CDN_BASE)下仅 game.worker.js 入档(engineExternal.files 仍记录引用的 runtime basename),Node 无头运行时优先用版本严格匹配的本地 @1game/engine-bundle,否则再从 CDN 拉取。

录制运行时:归档内的 game.worker.js 必须包含 @1game/engine-bundle/runtime/record(sql.js / 回放 RPC)导入。create 会自动传入 --with-record。后续 step / frame / bundle-player-html / @1game/test openGame 都依赖该 worker;手工 1game build 的纯游玩包默认不含 record,不能直接拿去当归档 worker。

示例:

  • 1gameplay create --entry src/game.tsx --out out/demo.1gamerecord

create 成功输出会附带 sceneStableUids(当前所有 scene 的 stableUid)。单 scene 时后续 --click / hit:point=x,y / --event 可省略 uid;多个 scene 正在渲染时省略会失败,须复制 uid 或传 --scene-stable-uid

关键参数:

  • --entry:游戏入口文件路径(必填)
  • --out:输出 .1gamerecord 文件路径(必填)。若同路径已有 <archive>.swp,创建会失败(先 mv/rm 暂存)。

成功输出:stdout 一行 JSON,包含 okarchivePathframeCount,以及 bundle.sha256

创建分支:branch create

在指定帧 fork 一条新 ref(不复制帧)。必须 --name 语义化分支名(不可与已有 label 重复,也不能写成生成 branchId)。省略 --at 时在父分支 HEAD fork;--from-branch 默认档案 activeBranchId。成功后该新 ref 成为 active,后续 step 在其上 append。

  • 1gameplay branch create out/demo.1gamerecord --name 探活岔路
  • 1gameplay branch create out/demo.1gamerecord --name 探活岔路 --at 12 --from-branch main

2) 继续步进:step

用途:在已有 .1gamerecord 上继续执行逻辑帧,可注入事件。

示例:

  • 1gameplay step out/demo.1gamerecord --ms 16.666
  • 1gameplay step out/demo.1gamerecord(不传 --ms 时默认 100
  • 1gameplay step out/demo.1gamerecord --ms 16 --repeat 10
  • 1gameplay step out/demo.1gamerecord --ms 16 --until store:state:anim.phase=idle --until-ms 800
  • 1gameplay step out/demo.1gamerecord --click 100,100
  • 1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"hover","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":1,"y":2,"time":0,"domClientX":1,"domClientY":2}}'
  • 1gameplay step out/demo.1gamerecord --event '{"type":"click","sceneStableUid":"<sceneStableUid>","data":{"x":100,"y":100,"ms":200}}'
  • 1gameplay step out/demo.1gamerecord --event '{"type":"swipe","sceneStableUid":"<sceneStableUid>","data":{"from":{"x":144,"y":220},"to":{"x":144,"y":170},"ms":300}}'
  • 1gameplay step out/demo.1gamerecord --event '{"type":"keypress","sceneStableUid":"<sceneStableUid>","data":{"code":"ArrowLeft","ms":180}}'
  • 1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.down","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":100,"y":100}}'
  • 1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.move","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":116,"y":100}}'
  • 1gameplay step out/demo.1gamerecord --ms 16 --event '{"type":"pointer.up","sceneStableUid":"<sceneStableUid>","data":{"id":1,"x":116,"y":100}}'
  • 1gameplay step out/demo.1gamerecord --ms 16 --event-file ./events.json
  • 1gameplay step out/demo.1gamerecord --ms 16 --from-frame 240
  • 1gameplay step out/demo.1gamerecord --ms 16 --repeat 60 --perf summary
  • 1gameplay step out/demo.1gamerecord --ms 16 --repeat 10 --perf frames
  • 1gameplay step out/demo.1gamerecord --dry-run --ms 16 --repeat 2

关键参数:

  • --dry-run:可选,写入 <archive>.swp(不存在则从主档复制创建);有 .swp 时无旗标写主档会失败(见上文「--dry-run / .swp」)
  • --ms:每步毫秒数,需 >= 0(可选,默认 100);注入 click/swipe/keypress 动作宏时该参数不生效(见下文)。--until 模式必须显式传入且 > 0
  • --repeat:开环步进次数,默认 1不能与 --until 同时使用
  • --until:谓词停机(store:state:<dotted-path>=<scalar> 或完整 hit:point= / hit:sceneStableUid=:uid=)。每空 tick 后求值,真则停。--until-not 取反。与 --repeat 互斥。
  • --until-ms:until 空 tick 的仿真时间上限(累计 --ms,不含动作宏微帧)。until 模式必填且 > 0。hit until 需要 --surface display
  • --clickx,y 点击动作宏糖(data.ms 默认 200;不能与 --event / --event-file 同时出现)。负坐标用 --click -10,20--click=-10,20
  • --scene-stable-uid:只给省略了 sceneStableUid 的注入事件填 uid(须为当前帧真实 scene uid);已写在 JSON 上的 uid 不改写;未省略时不校验该旗标。
  • --event:单个事件 JSON 字符串
  • --event-file:事件文件路径(单个对象或数组)
  • 省略 sceneStableUid 时(--click--event 都适用)仅当当前帧恰好一个 scene 才填充。多 scene 必须显式 uid 或 --scene-stable-uid。stdout meta.invokeSummary.eventSceneStableUids / defaultSceneStableUid 带回实际 stamp。
  • 事件类型支持 runtime 事件 touch|hover|keyboard|wheel|visibleChange;另支持瞬时宏事件 keydown|keyup|pointer.down|pointer.move|pointer.up(CLI 会按统一 InputSnapshot 差分展开;键盘与触摸均支持跨 step / from-frame 恢复),以及跨多帧的动作宏 click|swipe|keypress(见下文)
  • --from-frame:从指定帧裁剪后续再继续执行(只删当前分支独占的后缀;其它 ref 占用该后缀则失败)
  • --branch:在该 record_branches ref 上 append(不 fork;缺省 activeBranchIdmain

动作宏 click / swipe / keypress(跨多帧)

clickswipekeypress 描述的是一个完整的多帧动作data.ms 是这个动作从开始到结束经过的真实引擎时间,CLI 会自动拆成多帧分别注入并各自 tick,而不是把按下/抬起压缩进同一帧。单次 step 调用里,click/swipe/keypress 不能与其它事件混用(会拆分成多帧、与该次调用里的其它事件产生歧义),也不受该次调用的 --ms/--repeat--repeat 仍生效,含义是"重复整个动作 N 次",但 --ms 被动作自身的 data.ms 取代):

  • clickdata = { x, y, ms?=16, pressMs?=ms/2, id?, time?=0, domClientX?, domClientY? }。展开为 2 帧:第一帧注入按下并 tick pressMs;第二帧注入抬起并 tick ms - pressMs(两帧 tick 之和精确等于 ms)。
  • swipedata = { from:{x,y}, to:{x,y}, ms?=300, frames?, id?, time?=0 }。展开为 frames 帧(默认按 ms/16 估算并夹在 [3, 60] 之间,可用 frames 覆盖,允许范围 [2, 60]):第一帧 pointer.down@from,中间帧按位置线性插值 pointer.move,最后一帧 pointer.up@to,每帧 deltaMs = ms/frames(余数计入最后一帧)。注意frames=2(无中间 pointer.move)不会触发引擎的 hasGestureMoved/DragStart 判定,只有位置直接跳变,一般不建议使用。
  • keypressdata = { code, key?, shift?, meta?, alt?, ctrl?, ms?=16, pressMs?=ms/2, time?=0 }。展开为 2 帧,与 click 结构完全对称:第一帧注入 keydown 并 tick pressMs;第二帧注入 keyup 并 tick ms - pressMs(两帧 tick 之和精确等于 ms)。按住期间(两帧之间)InputSnapshot.keyboard 中该 code 保持存在,可跨真实引擎帧被 useFrame 等逻辑观察到。

示例:

# 点击(单 scene 可省略 uid;--click 默认按住 200ms)
1gameplay step out/demo.1gamerecord --click 180,320
1gameplay step out/demo.1gamerecord --event '{"type":"click","data":{"x":180,"y":320,"ms":200}}'

# 滑动(300ms 内从 (144,220) 滑到 (144,170),默认约按 60fps 拆成多帧)
1gameplay step out/demo.1gamerecord --event '{"type":"swipe","sceneStableUid":"<sceneStableUid>","data":{"from":{"x":144,"y":220},"to":{"x":144,"y":170},"ms":300}}'

# 按键(等价于按住 ArrowLeft 180ms 后松开,产生 2 帧)
1gameplay step out/demo.1gamerecord --event '{"type":"keypress","sceneStableUid":"<sceneStableUid>","data":{"code":"ArrowLeft","ms":180}}'

低层 pointer.down/pointer.move/pointer.up/touch/keydown/keyup 宏仍然保留,供需要精细控制(自定义 domClientX/Y、多指、跨命令分帧按住/移动/释放)的场景使用;click/swipe/keypress 只是构建在其上的高层封装。frame batch 不支持 click/swipe/keypress(会返回 INPUT_ACTION_MACRO_UNSUPPORTED),请改用底层宏。试跑不写主档请用 step --dry-run

  • --capture-console:采集 Worker 内 console 到 SQLite runtime_logsoff|error|warn|info|log|debug|all默认 warn)。采集为旁路元数据,不影响回放状态;原 console 仍会照常输出到进程 stderr。
  • --console-max-per-step:每步最多保留的日志条数(默认 50),超出部分丢弃并计入 invokeSummary.consoleDroppedByLimit
  • --console-max-bytes:单条 message JSON 序列化后的最大字节数(默认 4096),超出截断并标记 truncated
  • --perf:Host Lab 性能采集off|summary|frames|trace默认 off)。日常解读用 obs report --profile(只读,不采样)。summary 写入 stdout meta.perf.summary 并落一行 1gameplay.perf.summary/1(挂该次 lastSeq);frames/trace 写逐帧 1gameplay.perf.frame/1不写 summary 行。旧 --with-metrics 已移除。含 meta.captureSession 的真机档禁止再 --perf(硬拒发生在 .swp 之前;无源码请 obs report --profile device.budget)。
  • --perf-max-frames:截断 stdout meta.perf.frames 条数(>= 0;不影响已写入 runtime_perf 的行)。
  • --surface:Host Lab 面(record|display默认 record)。record 关 publish;display 开 publish。--flushflushFrame 等待记入 host.flushWaitMs不是浏览器 paint)。invokeSummary.surface 回显 record|display

pointer.* 跨命令恢复的 stableUid 契约:

  • 跨包冻结语义与 strict 默认行为以 ../../docs/stable-uid-identity-contract.md 为准。

  • 构建期为每个 DSL 节点注入内部 __sid,运行时派生全局唯一的 stableUid(路径拼接)。

  • 一致性边界:stableUid 仅保证同一构建产物在多次运行中的节点一致;跨构建或跨源码改动后不保证保持同一 stableUid。

  • 输入快照中持久化 targetStableUid / downStableUid,跨 step--from-frame 时用于回查节点。

  • 不支持跨源码位点(如 Show 主分支与 fallback)视为同一实体;scene vnode replacement 切换分支后,onClick / onDrag* 语义不再保证连续。

  • 生成期冲突(生成重复 stableUid)属于硬错误:运行时直接抛 E_STABLE_UID_COLLISION(dev/prod 一致)。

  • 消费期 duplicate 去重策略默认值:非 productionstrictproductionfirst-wins,可通过 ONEGAME_DUPLICATE_STABLE_UID_STRATEGY 覆盖(非法值回退默认)。

  • ONEGAME_STEP_STORE_DUMP_FULL=1step 每帧强制全量 debug.dumpStore(关闭 dumpRange watermark;用于排障 / A/B)。

  • debug.dumpStore(RPC v9 硬切):返回值必须含 metamode/fromIndex/length/pruneEpoch);includeCurrentState 默认 falseframe query --select store:state 会显式传 true)。无 meta 的旧 Worker 不再被接受。

成功输出:stdout 为 JSON envelope,schema1gameplay.stepok: truemeta.statePointer 含与 manifest.runtime 对齐的 lastSeqframeCountlastTickedTimeMs(便于后续 frame query --atframes list 无需猜 --at)。meta.invokeSummary 含本次实际生效的 msEffectiverepeatEffective(until 模式为 0)、until(开环为 null;until 成功含 met/ticks/simMs/lastValue)、是否 --from-frame 截断、注入的 --event / --event-file 数量、clickInjectedeventSceneStableUidsdefaultSceneStableUid,以及 captureConsoleconsoleCapturedconsoleDroppedByLimitsurface。启用 --perf 时额外含 meta.perfsummary 恒有,含 phases/counters/host/derived;frames/trace 另附 frames 数组)。

Host 解读:hostOverheadMs = frameWallMs − workerTotalMs(冻结)。storeDumpMs 高而 workerTotalMs 极低时,墙钟主导是 journal/host,不是仿真。

失败:step 使用与 frame 一致的 1gameplay.error 信封,并默认写入 stdout。退出码:2 参数(含 STEP_UNTIL_PATH_MISSING / STEP_UNTIL_PATH_INVALID / CAPTURE_SESSION_HOST_LAB_REFUSED);3 缺失路径等;4 记录/bundle 不兼容;5 回放运行时错误;6 STEP_UNTIL_NOT_MET(仿真预算用尽,谓词仍假;已步进的帧会提交)。

2b) 只读性能报告:obs report

用途:按剖面读取 Worker / 真机 Main rAF / paint-lab 三只时钟桶。不采样。信封 1gameplay.obs.report/1 只出现在 stdout / --out

1gameplay obs report userplay.1gamerecord --profile device.budget --at spike
1gameplay obs report out/lab.1gamerecord --profile lab.worker
1gameplay obs report out/lab.1gamerecord --profile lab.paint --at last
  • Agent 必须显式 --profilelab.worker|lab.display|lab.paint|device.budget)。CLI 默认:有 captureSessiondevice.budget,否则 lab.worker
  • device.budget 需要真机信封;无信封实验室档用 lab.worker(须已有 1gameplay.perf.* 行)。
  • 有信封时 stdout meta.captureSessionframe query 同键(1game.capture.session/1);Main rAF 读 buckets.main。缺档是 NOT_FOUND,不要当成 CAPTURE_SESSION_REQUIRED
  • 不要跨桶比较毫秒。可见超预算 rafSpikes 须与 Worker seq / 点击互证。

3) 添加帧评论:comments add

用途:向已有 runtime 帧附加一条 frame_comments 记录(与 schemaVersion 仍为 3 的归档兼容)。

示例:

  • 1gameplay comments add out/demo.1gamerecord --at last --body "checkpoint ok"(省略 --author-idauthorId: null
  • 1gameplay comments add out/demo.1gamerecord --at 12 --body "review" --author-id ai

关键参数:

  • --atlast 或具体 seq(必须是档案中存在的帧)
  • --body:评论正文(必填)
  • --author-id / --anchor-json / --comment-id:可选。Agent 钉锚点请传 --author-id ai;回放页人手新建/改过的评论为 user

成功输出:stdout 为 JSON envelope,schema1gameplay.comment.v1result.comment 为写入后的记录。

4) 列出帧:frames list

用途:以 JSON envelope 列出帧摘要。

示例:

  • 1gameplay frames list out/demo.1gamerecord
  • 1gameplay frames list out/demo.1gamerecord --from 100 --to 160

参数约束:

  • --from / --to 必须是 >= 0 的整数,且 from <= to

成功输出:stdout 为 JSON envelope,schema1gameplay.frames.list.v1result.columns 固定为 seq|deltaMs|tickedTimeMs|events|storeCursorresult.rows 为范围内帧摘要数组。

5) 帧调试:frame query | diff | batch

用途:以稳定 JSON envelope 查询/对比/批处理;无隐式 fallback(缺失 event、缺失 store、缺失 scene 均报错)。

成功 1gameplay.frameresultselect 外,还包含 commentsBySeq:以帧 seq 字符串为键、该帧关联的 frame_comments 行数组(query 为单键;diff--from--to 两端;batch 每条成功任务的 result 各自携带,避免任务间混淆)。

--select DSL(可重复):

  • 所有 sceneStableUid 入参都必须传 render 输出里的 scenes[].stableUid(或 sceneStableUids[]);不再接受 scene name 的隐式匹配。
  • 只要本次 frame query--select 触发了 worker 回放(例如 renderstore:frame/store:statehit:*),响应的 meta.sceneStableUids 就会自动带出当前所有 scene 的 stableUid,省去单独再查一次 render。仅用 runtime/events/store:dump 等无需回放的 lite 路径查询时不会附带(避免为此多启动一次 worker)。
  • runtime:该 seq 的 runtime 行
  • events:该 seq 的事件列表
  • logs:该 seq 的 runtime_logs 旁路行
  • perf:该 seq 的 runtime_perf 旁路行(需先前 step --perf frames|trace 写入)
  • store:dump:从记录文件的 store 表读行(不回放 worker)
  • store:frame / store:state:store-only 架构约定下同样从 journal 纯计算得出(不回放 worker);仅当同一次查询还选择了 render/hit/hitNode 时才会随之启动 worker
  • render:全量 render 摘要(--payload full 时返回完整对象)
  • render:sceneStableUid=<sceneStableUid>:仅该 scene;scene 不存在则失败(sceneStableUid 为 CLI 字段名兼容保留,值语义为 scene stableUid)
  • hit:point=<x>,<y>[:sceneStableUid=<sceneStableUid>][:mode=clickable|any][:pick=top|all]:坐标命中测试。省略 sceneStableUid 时当前帧必须恰好一个 scene。返回 { stableUid, stableChain, sceneStableUid }(未命中时 stableUid: null
  • hit:sceneStableUid=<sceneStableUid>:uid=<stableUid>[:shape=polygon|aabb][:space=scene|local][:include=...]:节点区域/可点击性快照;uid=* 表示 scene 下首个非 scene 节点。stableUid 片段格式为纯 sid(静态唯一位点)或 sid@k_<encodeURIComponent(String(key))>(列表 key);列表必须提供稳定 key。旧 node= 不再支持。

示例:

  • 1gameplay frame query out/demo.1gamerecord --at last --select runtime --select store:dump
  • 1gameplay frame query out/demo.1gamerecord --at 1 --select perf
  • 1gameplay frame query out/demo.1gamerecord --at last --select 'hit:point=20,20:sceneStableUid=<sceneStableUid>'
  • 1gameplay frame query out/demo.1gamerecord --at last --select 'hit:sceneStableUid=<sceneStableUid>:uid=*:shape=aabb:include=meta,path,aabb'
  • 1gameplay frame diff out/demo.1gamerecord --from 0 --to last --select store:dump
  • 试跑不改主档:1gameplay step out/demo.1gamerecord --dry-run --ms 16,再 frame query out/demo.1gamerecord --dry-run --at last --select store:state
  • 1gameplay frame batch out/demo.1gamerecord --plan-file ./plan.json

计划文件:schema 必须为 1gameplay.frame.plantasks[]taskIdtypequery | diff)及对应字段。

frame 子命令退出码:0 成功;2 参数/越界 seq;3 对象不存在;4 归档/bundle 兼容性;5 回放运行时错误。

6) 帧截图:frame screenshot

用途:在 Node 主线程用 @napi-rs/canvas 回放指定帧并写出 PNG/JPEG。不依赖 Chromium / Playwright

示例:

  • 1gameplay frame screenshot out/demo.1gamerecord --at 0 --out out/frame0.png
  • 1gameplay frame screenshot out/demo.1gamerecord --at last --out out/last.jpg --format jpeg
  • 1gameplay frame screenshot out/demo.1gamerecord --at last --out out/scene.png --scene-stable-uid <sceneStableUid>

关键参数:

  • --atlast 或具体 seq(必填)
  • --out:输出图片路径(必填;扩展名可暗示格式)
  • --scene-stable-uid:可选,仅截取该 scene
  • --width / --height / --dpr:视口与设备像素比(默认 800×600dpr=1
  • --formatpng | jpeg(默认由 --out 扩展名推断,否则 png
  • --quality:JPEG 质量 1–100(默认 90;PNG 忽略)

成功输出:stdout 为 JSON envelope,schema1gameplay.screenshot.v1,含 resolvedSeqimage.{format,width,height,dpr,bytes}stats(可选 resourceWaitTimedOut)。

6b) 绘制快照:frame paint

用途:同一 display 宿主(Node @napi-rs/canvas)对指定 seq 做一次 Canvas 绘制采集。stdout JSON(1gameplay.paint.snapshot/1),不写 PNG。量 scene / byType / 截断 topNodes不是 Host Lab step --perf,也不是浏览器 FPS。

示例:

  • 1gameplay frame paint out/demo.1gamerecord --at last
  • 1gameplay frame paint out/demo.1gamerecord --at last --top 0
  • 1gameplay frame paint out/demo.1gamerecord --at last --scene-stable-uid <sceneStableUid>

关键参数:--at 必填;禁止 --out--top 默认 50–200 省略 topNodes);视口默认与 screenshot 相同(800×600dpr=1)。

成功信封含 backend: "napi-canvas"notFps: truescenes[](overlay scene 的 nameoverlay: 开头)、topNodesstats.paintMs(各 scene 强制 getRenderedFrame 之和,不含 compositeMs / PNG encode)。stats.forcedRedraw 只表示 scene canvas 被强制重绘,不会打掉 render.drawCache;cache 子树计在 nodesCacheElided,不要当成 nodesCulledstats.compositeMs 是 harvest 之后把已画好的 scene 帧 drawImage 到主画布,不是第二次 engine.doRender。不要和 meta.perf / captureSession.main.raf* 对比。禁止 --out / --select / --format / --quality / --payload。极简场景 selfMs 可能为 0。

默认中文字体:依赖 @fontpkg/source-han-sans-hw-sc(思源黑体 HW SC Regular + Bold),在 Node 主线程与 Worker 启动时注册;场景未配置 textMeasureFontFamily 时主线程默认使用 Source Han Sans HW SC

已知差异(相对浏览器回放):

  • 像素与浏览器 Canvas 不完全一致(字体、抗锯齿、合成路径不同)
  • 远程 URL 图片在无头 Node 路径下不会绘制
  • 依赖 GPU/WebGL 上下文的滤镜在 headless 主线程会被跳过/降级(无头路径是 Canvas 2D / @napi-rs/canvas,不是仓内实验 WebGL 后端)

7) 删除帧:frames delete

用途:在原 .1gamerecord 上按区间删除 runtime frames,可选 compact;需要保留原文件时请先复制或使用 record extract 生成新文件。

示例:

  • 1gameplay frames delete out/demo.1gamerecord --ranges 100-300,450-500
  • 1gameplay frames delete out/demo.1gamerecord --ranges 100-300 --compact
  • 1gameplay frames delete out/demo.1gamerecord --last 3
  • 1gameplay frames delete out/demo.1gamerecord --last 3 --compact

关键参数(--ranges--last 二选一):

  • --ranges:格式如 100-300,450-500
  • --last:删除末尾 n已存在的 runtime 帧(n >= 1 整数,且不得超过当前帧数);按 seq 升序取尾部并合并为 deletedRanges(连续档案通常为单段 { from: maxSeq-n+1, to: maxSeq };有缺口时可为多段)
  • --dry-run:可选,改写 <archive>.swp(须已由 step --dry-run 创建);有 .swp 时无旗标写主档会失败
  • --compact:可选,重写并压缩 store journals

成功输出:stdout JSON,包含 deletedRangesruntimeFrameCounteventCount;使用 --last 时额外含 last--dry-run 时额外含 dryRun / effectivePath

8) 抽取帧区间:record extract

用途:从已有 .1gamerecord 中抽取连续 runtime 帧区间,写成新的独立 .1gamerecord 文件;输入文件不会被覆盖。若 --out 指向已有文件,会按当前写入策略覆盖该目标文件。

示例:

  • 1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160
  • 1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --merge-diff
  • 1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --merge-diff 3
  • 1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-seq 100 --to-seq 160 --branch-meta
  • 1gameplay record extract out/demo.1gamerecord --out out/clip.1gamerecord --from-ms 1000 --to-ms 2600

关键参数:

  • --out:新 .1gamerecord 输出路径,不能与输入路径相同;目标文件已存在时会被覆盖
  • --from-seq / --to-seq:按 runtime seq 抽取闭区间
  • --from-ms / --to-ms:按 tickedTimeMs 解析到不超过该时间的 runtime seq(at-or-before)后抽取闭区间;早于首帧时会解析到首帧
  • --merge-diff [n]:可选,合并未被硬锚定的连续 store diff,并删除因此失去 store 终点的 runtime 帧(这些时刻不可再 seek)。默认 n=2(相邻 2 条可合并 diff → 1 条);可传 n>=2。某一连续可合并段长度不足 n 时不合并该段尾巴,继续向后扫描。硬锚点:full、窗口首/末 runtime 的 storeCursor、带评论帧的 storeCursor
  • --branch-meta:可选,在输出档案写入 provenance:输入文件字节 SHA-256 与原始 fromSeq;默认不继承输入档案的分支元数据

frames delete 的区别:frames delete 会就地修改输入档案;record extract 会写出一个新的独立档案,并保留输入档案不变。

成功输出:stdout JSON envelope,schema1gameplay.record.extract.v1,包含解析后的 fromSeqtoSeqmergeDiffmergeDiffCountbranchMetaframeCountcommentCount、store / runtime 行数变化(--merge-diffframeCount 可能小于抽取窗口);传入 --branch-meta 时还包含 originRecordShabranchedFromSeq

9) 导出回放页:bundle-player-html

用途:默认在 --out 位置生成单文件 replay.html(内联薄 ESM player.bundle.js,并对 .1gamerecord 先 gzip 再 base64 写入 recordBase64,同时带 recordEncoding: "gzip",不写 sidecar 文件)。单文件下引擎解析由 ONEGAME_ENGINE_CDN_BASE 切换(与 1game build --target single-file 同规则):已设 → HTML import map 指向 CDN runtime-main.mjs未设 → 内嵌本地 dist/engine/runtime-main.mjs,经典 boot 生成 blob: 再写 import map。若传 --multi-file,则在 --out 所在目录生成 replay.htmlplayer.bundle.jsengine/runtime-main.mjs 以及 .1gamerecord 的副本(多文件模式不做外层 gzip,可离线静态托管)。

sql.js WASM 地址在导出时写入 gr-config.wasm

{ONEGAME_ENGINE_CDN_BASE|https://cdn.jsdelivr.net/npm}/sql.js@{installedVersion}/dist/sql-wasm.wasm

复用 ONEGAME_ENGINE_CDN_BASE(未设则 sql.js 仍默许 jsDelivr,与引擎 getEngineCdnBase()===null 语义分离);版本对齐导出进程安装的 sql.js。单文件导出时原样内联已发布的薄 ESM 壳,不改写壳字节、不跑 esbuild。引擎 local ./engine/ / CDN / inline-blob 规则见 docs/engine-cdn-resolution.md

导出页会在 gr-config 里默认写入两个可替换模板值:

  • shareButtonText: "__1GR_SHARE_BUTTON_TEXT__"(分享按钮文案)
  • shareActionMessage: "__1GR_SHARE_ACTION_MESSAGE__"(iframe 分享消息类型)

你可在产出的 replay.html 上替换这两个固定字符串来自定义。若 shareActionMessage 有值且页面被 iframe 加载,评论区「分享」(须有评论)或 userplay 暂停后更多菜单的采集分享(须已游玩)会向父窗口发送 postMessage({ type, fileBuffer, commentsJson, filename, kind })kindcommentscapture;否则走默认下载。采集分享不要求评论。

示例:

  • 1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html(未设 CDN → inline-blob)
  • ONEGAME_ENGINE_CDN_BASE=https://cdn.jsdelivr.net/npm 1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html
  • 1gameplay bundle-player-html out/demo.1gamerecord --out out/replay.html --multi-file

打开 replay.html?mode=autoplay 进页自动播放(可暂停);?mode=userplay 打开玩家游戏采集面板(空回放时只留不可关闭的进入游戏浮层,隐藏进度条);?userplay=0 隐藏「进入玩家游戏模式」菜单(忽略 mode=userplay)。

关键参数:

  • --title:可选,浏览器页标题
  • --single-html:可选,显式指定单文件模式(默认即单文件;CDN 或 inline-blob)
  • --multi-file:可选,离线多文件模式(replay.html + player.bundle.js + engine/runtime-main.mjs + 归档副本)
  • --dry-run:可选,从已有 <archive>.swp 读归档导出(不改主档)
  • --shell:已移除。传任意值(含 play-only / full)会失败;自动播用 replay.html?mode=autoplay

构建前提:需先在仓库根构建播放器壳包:pnpm --filter @1game/1gamerecord-player-html build(否则 CLI 会报缺少 player.html / player.bundle.js--multi-file 与 inline-blob 另需 engine/runtime-main.mjs)。

输出与错误约定

  • 所有命令默认输出 JSON(成功为对应 schema,失败为 1gameplay.error),并统一写入 stdout
  • frame query|diff|batch:成功与失败均向 stdout 输出 JSON(1gameplay.frame / 1gameplay.error),退出码见上文。
  • frame screenshot:成功 1gameplay.screenshot.v1frame paint:成功 1gameplay.paint.snapshot/1。失败均为 1gameplay.error
  • frames list:成功 schema 为 1gameplay.frames.list.v1

常见问题

1) 报错:bundle-player-html 需要 --out <replay.html>

请补齐 --out 参数。

2) 报错:engineVersion 不兼容

这表示归档版本与当前运行版本不兼容。请使用与归档匹配的 CLI / 引擎版本,或重新录制。

3) 报错:--ms 必须是 >= 0 的数字--repeat 必须是 >= 1 的整数

请检查 step 参数取值。--until 必须搭配 --until-ms 与显式 --ms,且不能同时传 --repeat

3b) 报错:STEP_UNTIL_NOT_MET

谓词在 --until-ms 内仍为假。查 details.lastValue / frame query --select store:state;加大 --until-ms 或修正 path。这不是 worker 超时(那是退出码 5)。

4) 报错:better-sqlite3.node is missing / Cannot find module 'better-sqlite3'

1gameplay 在 Node 环境下会使用 better-sqlite3 读写 .1gamerecord。如果你的 pnpm 环境启用了构建脚本限制(例如忽略未授权构建),可能导致 better-sqlite3 未完成原生构建,从而在运行时抛出该错误。

可选修复方式:

  1. 一次性审批当前项目的构建脚本(推荐):

    • pnpm approve-builds
  2. 在项目 package.json 中显式允许构建 better-sqlite3(适合团队统一配置):

    • 在根 package.json 增加:
{
  "pnpm": {
    "onlyBuiltDependencies": ["better-sqlite3"]
  }
}

完成后重新安装依赖:

  • pnpm install