# 需求综述 需要一个能录制并回放游戏过程的系统,通过录像唯一还原一段游戏流程。 # 需求理解 ## 明确需求 1. 游戏过程可回放。能通过录像唯一地还原出游戏从起点到终点的一段完整流程。 2. 录像步可自定义。录像步的类型不限于系统内置——移动、使用道具等——也允许作者自定义。 ## 隐含需求 1. 异步播放。由于播放每个录像步都伴随渲染动画,动画未结束前不能执行下一步——播放必须是异步的。 2. 渲染端被动通知。架构约束下渲染端不能主动拉取数据——需要钩子将每步信息推送给渲染端。 3. 录像需跨会话持久化。录像可能录制于这次游戏,在下次游戏中被播放——需要存档。 ## 未确定需求 无。 # 设计前提 1. 确定性执行。计入录像的操作在相同输入下产生相同输出。 2. 单向依赖。录像系统位于 `@user/data-common`,不依赖 Layer 1/2。渲染端被动接收信息。 3. 外部主动录制。系统不自动录制——作者在逻辑系统中手动调用录像步写入接口。 4. 底层存储独立。录像的二进制存储由 `IReplayArray` 统一管理(详见 `replay-array.md`),本系统不直接操作 `ArrayBuffer`。 # 核心概念定义 ## 录像步 一段游戏流程中不可再分的最小操作单元。由一个指令标识和一组数量可变的参数构成:指令标识区分操作类型,参数描述操作的细节。 ## 录像 一组按时间顺序排列的录像步的序列。完整记录从某初始状态到某最终状态的全部操作,是还原一段游戏流程的充要条件。 ## 录像录制 将玩家操作转化为录像步并通过 `IReplayArray.add` 追加到末尾的过程。由作者在各逻辑系统(商店、移动等)中主动调用写入接口完成。 ## 录像播放 从 `IReplayArray` 中通过流式读取器依次取出录像步并驱动数据端状态变化的过程。由于播放时需要从初始状态逐步重建到目标状态——这是整个系统中唯一需要批量修改全局状态的场景——使用沙箱隔离播放过程。 # 接口设计分析 ## `IReplaySystem` ### 设计思路 由于要还原游戏流程,需要记录玩家操作。不同操作对应不同录像步类型——因此需要 `registerCommand` 注册命令,使每种命令 ID 映射到对应的执行对象。 注册了命令类型后,运行时需要向 `IReplayArray` 中追加新步——因此需要 `record` 方法,接收命令 ID 和可变长度的参数,内部委托给 `IReplayArray.add`。 由于需要回放录像,且回放时原始录像系统仍需录制新步(做交叉验证),播放需要有独立的执行环境。此外播放前需要重置全局状态,但重置逻辑属于数据端不应被 Layer 0 直接依赖——因此需要 `createReplaySandbox` 创建沙箱,通过 `IStateReseter` 接口间接访问重置能力。播放结束需要销毁沙箱——因此需要 `releaseSandbox`。 由于播放期间渲染端和数据端需要切换行为,需要一个成员标记当前是否有活跃沙箱——因此需要 `replaying`。外部可能需要获取当前沙箱实例——因此需要 `sandbox` 成员。 ### 接口分析 - 成员 `replaying: boolean`:预期频率**高频**。 - 成员 `sandbox: IReplaySandbox | null`:预期频率**中频**。 - 方法 `registerCommand(id: number, command: IReplayCommand)`:预期频率**低频**。 - 方法 `record(id: number, ...params: ReplayParamValue[])`:预期频率**高频**。 - 方法 `createReplaySandbox(reseter: IStateReseter, save?: Map): IReplaySandbox`:预期频率**低频**。 - 方法 `releaseSandbox()`:预期频率**低频**。 ### 预期体量 预期代码体量 60-80 行。 - 命令注册与管理预期 15 行:`Map` 的增删查。 - 沙箱创建与销毁预期 20 行:深拷贝 `IReplayArray`、注入命令注册表和 `IStateReseter`、创建 `ReplaySandbox` 实例。 - 存档实现预期 25 行:`saveState` 从 `IReplayArray` 导出 `ArrayBuffer` 及 `IReplayMetadata`,`loadState` 从存档数据重建 `IReplayArray` 并恢复标记。 ## `IReplayCommand` ### 设计思路 由于作者可以自定义录像步类型,播放不同步类型时需要执行不同的操作逻辑。录像系统本身无法知晓这些逻辑,需要作者提供。这些逻辑往往需要持有数据端引用,纯函数回调无法满足——因此需要接口对象 `IReplayCommand`,作者编写实现类通过构造器持有所需引用。沙箱执行时传递当前步信息——因此 `execute` 直接接收 `IReplayStepHandler`。 ### 接口分析 - 方法 `execute(step: IReplayStepHandler)`:预期频率**低频**。每个命令编写一个实现类。 ### 预期体量 预期代码体量 5 行。 ## `IReplaySandbox` ### 设计思路 由于需要回放录像,播放过程中需要流程控制——连续推进、暂停、单步前进、终止。因此需要 `play`、`pause`、`resume`、`step`、`stop` 五个方法。 由于需求隐含要求异步播放和渲染端被动通知——项目已有 `IHookable` 通用钩子系统,沙箱扩展 `IHookable`,每步触发钩子并 `await` 其完成。 由于播放期间原始录像系统仍在录制,沙箱不能引用正在增长的 `IReplayArray`——创建时深拷贝,播放时通过 `IReplayReadStream` 顺序读取每一步,避免每次随机访问的 O(n) 偏移扫描。播放速度需要调节,渲染端据此调整动画时长——因此需要 `speed` 只读成员和 `setSpeed` 方法。 ### 接口分析 - 成员 `replaying: boolean`:预期频率**高频**。 - 成员 `pausing: boolean`:预期频率**中频**。 - 成员 `speed: number`:预期频率**中频**。播放倍率,只读。 - 方法 `setSpeed(speed: number)`:预期频率**低频**。 - 方法 `play()`:预期频率**低频**。 - 方法 `pause()`:预期频率**低频**。 - 方法 `resume()`:预期频率**低频**。 - 方法 `stop()`:预期频率**低频**。 - 方法 `step()`:预期频率**中频**。返回 `Promise`。 ### 预期体量 预期代码体量 120-160 行。 - 播放循环预期 80 行。在循环中通过 `IReplayReadStream.read()` 逐帧解码,传给 `IReplayCommand.execute`,再触发 `onStep` 钩子并 `await`。连续模式下循环自动推进,单步模式执行一步后挂起等待下个 `step` 调用,暂停冻结循环但不丢失索引,终止跳出循环并清空流式读取器。四种模式的状态切换组合逻辑占主要行数。 - 状态管理预期 30 行。`play`/`pause`/`resume`/`stop`/`step` 之间的互斥与合法性检查——未播放时 `pause` 无效果,已暂停时 `resume` 才恢复,`stop` 后一切操作无效。 - 钩子集成预期 20 行。扩展 `Hookable` 后在循环中调用 `forEachHook`,收集 `onStep` 的 Promise 并全部 `await`。 ## `IStateReseter` ### 设计思路 由于沙箱播放前需将全局状态恢复到基准状态——从头播放恢复为初始状态,从存档继续播放恢复为存档状态。重置能力属于数据端,不应被 `@user/data-common` 直接依赖——因此设计 `IStateReseter` 接口隔离此依赖,`reset` 接收可选存档数据覆盖两种场景。 ### 接口分析 - 方法 `reset(save?: Map)`:预期频率**低频**。 ### 预期体量 预期代码体量 3 行。 ## `IReplaySandboxHook` ### 设计思路 由于架构约束下渲染端被动接收信息,沙箱需要钩子将每步信息推送给渲染端。项目已有 `IHookable`/`IHookBase` 体系——因此沙箱钩子扩展 `IHookBase`,提供 `onStep` 方法接收 `IReplayStepHandler`。沙箱 `await` 其完成后再推进下一步。 ### 接口分析 - 方法 `onStep?(step: IReplayStepHandler): Promise`:预期频率**低频**。渲染端注册时定义一次。 ### 预期体量 预期代码体量 4 行。 ## `IReplayStepHandler` ### 设计思路 由于钩子和命令执行器都需要当前步的完整信息(操作类型、参数、索引),因此设计统一的 handler 风格数据接口 `IReplayStepHandler`,包含 `id`、`params`、`index`。 ### 接口分析 - 成员 `id: number`:预期频率**高频**。 - 成员 `params: readonly ReplayParamValue[]`:预期频率**高频**。 - 成员 `index: number`:预期频率**低频**。 ### 预期体量 预期代码体量 5 行。 ## 类型别名 `ReplayParamValue = number | string | boolean | bigint` --- # 实现思路 ## 沙箱播放与交叉验证 1. 创建沙箱时深拷贝当前 `IReplayArray`。 2. 调用 `reseter.reset(save)` 重置全局状态。 3. 创建 `IReplayReadStream`,循环:`read()` 解码一步 → `IReplayCommand.execute` → 触发 `onStep` → `await`。 4. 原始录像系统清空并重新录制——播放中沙箱执行的命令触发 `record`,若遗漏则安全系统警告。 # 涉及文件 ## `@user/data-common/src/replay/types.ts` - [ ] 新增 `IReplaySystem`、`IReplaySandbox`、`IReplayCommand`、`IStateReseter`、`IReplaySandboxHook`、`IReplayStepHandler` 接口与 `ReplayParamValue` 类型别名 ## `@user/data-common/src/replay/replay.ts` - [ ] 编写 `ReplaySystem` 类:命令注册、沙箱创建、存档实现 ## `@user/data-common/src/replay/sandbox.ts` - [ ] 编写 `ReplaySandbox` 类:扩展 `Hookable` 实现 `IReplaySandbox` ## `@user/data-common/src/replay/array.ts` - [ ] 详见 `replay-array.md` ## `@user/data-common/src/types.ts` - [ ] 新增 `IDataCommon` 成员 `replay` ## `@user/data-common/src/index.ts` - [ ] 新增 `export * from './replay'` ## `@user/data-state/src/core.ts` - [ ] 新增 `ReplaySystem` 实例化与 `IStateReseter` 实现 - [ ] 新增存档注册