11 KiB
录像一致性
录像验证的用法是:把玩家的操作录下来,之后只跑数据端、按顺序重放,看结果与原始游玩是否一致。要让这件事成立,前提是该记的行为一条都不能少。少记一条,重放时那一步就没有发生,之后每一步都建立在错误的状态上,而第一次出现差异的位置,往往离真正的原因很远。
中级教程的录像指令讲了录像是什么、主动与被动的区别、以及怎么新增一条指令。这一篇往下一层:@shouldReplay 与 @ignoreReplay 这两个装饰器该写在什么方法上,它们能替你查出什么、查不出什么。
阅读前需要会基础语法:class、new、import,以及装饰器写法 @名字(...)。装饰器的基础写法见代码基础。
什么该记
同一条录像,重放两次必须得到同样的数据端结果;否则录像验证就失去了意义。要做到这一点,只能靠「录像里记下的操作,恰好覆盖了所有会改变游戏状态的行为」。
核心原则在中级已经给过,这里再抄一遍,因为后面所有判断都从它出发:
:::info 核心原则 玩家直接产生、会影响最终成绩的行为,必须计入录像。 :::
两个条件缺一不可:由玩家亲手做出(移动、使用道具、装备、触发事件),并且会改变游戏结果。代码自己跑出来的、只影响画面的,都不算。
一个方法该不该记,通常是分析出来的,不是试出来的。先问三件事:
- 这个方法能不能被玩家直接触发?
- 它会不会改变游戏状态?
- 这个改变有没有可能影响最终结果?
三个答案都是「是」,就加 @shouldReplay。如果你暂时判断不了,最稳妥的做法是:把所有「修改了游戏状态、且该状态可能导致结果改变」的方法都加上标记。漏标的代价是录像悄悄错掉;多标的代价只是告警里多一条路径,前者难查得多。
只读查询、纯计算、展示与搜索路径不要标。它们不改变游戏状态,标了只会让告警列表变长,把真正的漏记埋进噪声里。
给会改状态的方法加标记
标记加在你自己的、会改状态的方法上。如果你不确定该把标记写在哪里,可以参考下面的模板:它把一个会改状态的公开方法标成「应计入录像」,其余成员用注释省略。商店插件是现成的例子,它的 ShopSystem 在 src/data/plugin/shop/system.ts 里给开店、关店、购买、出售都加了标记:
// shouldReplay 从数据端公共包引入
import { shouldReplay } from '@user/data-common';
import { IShopSystem, ShopTradeResult } from './types';
// 商店插件节选:完整类结构,未展示的成员用注释省略
export class ShopSystem implements IShopSystem {
// ... 其他成员
// 购买会改变金币与商店库存,必须计入录像
// 装饰器参数是写给人看的说明,出现在告警里
@shouldReplay('Buying items should be replayed.')
buyItem(item: number | string, count: number): ShopTradeResult {
// ... 原有的购买逻辑
}
}
这个模板声明了 ShopSystem 类,实现 IShopSystem 接口;@shouldReplay 紧贴在 buyItem 方法上方,装饰的是这个方法。你需要改的是:把类与方法换成你真正会改状态的那一个,把 message 写成这个方法改变了什么状态;同一个类里还有其它会改状态的入口时,按同样方式逐个标注。
装饰器紧贴方法声明上方,中间不要隔别的成员。shouldReplay 从 @user/data-common 引入;商店插件的文件顶部已经有这行 import,新文件才需要自己补。同一个插件里,openShop、closeShop、sellItem 也都按同样方式标注。
@shouldReplay 与 @ignoreReplay 只在数据端使用。录像验证只跑数据端,渲染端不参与重放,也不在安全检查的覆盖范围内;不要在渲染端代码里加这两个装饰器。
两个装饰器
两个装饰器都写在类的方法上方,作用对象是「录像安全检查」,不是录像本身。
| 装饰器 | 写在哪 | 做什么 |
|---|---|---|
@shouldReplay(message) |
会改变游戏状态的方法上 | 把「这个方法被调用过」登记成一条路径 |
@ignoreReplay(reason) |
确实不该计入录像的被动 / 延迟路径 | 把整段检查标记为忽略 |
:::warning 录像记录缺失
如果带 @shouldReplay 的方法执行了,而这次执行没有产生任何录像记录,控制台会输出一条「录像记录缺失」的警告,并列出这次执行经过的标记方法。看到它,说明这条路径该记却没记。检测的内部原理留到 高级 III · 录像深层原理。
警告里还会附一条可直接复制的调用,形如 Mota.require('@user/data-common').logReplaySafetyDetail(0);。把它粘到控制台执行,就能展开这次检查的完整调用路径,逐层看到经过的标记方法。这个调用形式之后可能会调整,以控制台实际打印的那条为准。
:::
@shouldReplay:只登记路径,不写录像
@shouldReplay(message) 把方法标记为「应该计入录像」。它不写录像:真正记录仍要靠 state.replaySystem.record(code, ...params),或者走内置的录制入口。它做的是让安全检查知道「这条路径执行过」,以便和录像里实际产生的步数对照。
message 是写给人看的说明,用来在警告里指认方法;语言和措辞不影响行为,写清这个方法改变了什么状态即可。
@ignoreReplay:忽略整段检查
@ignoreReplay(reason) 容易被误解成「只忽略这个方法」。实际不是:它把整段安全检查标记为忽略,包括同一段里其它正常登记的 @shouldReplay 路径。
所以它不能用来「消掉某条告警」。你消掉的是整段检查,真正的漏记会被一起放过。它只适合确实不该计入录像的路径,例如异步内容、延迟回调这类不会立刻改变状态的方法。源码注释也明确写了这层意思,并提醒不要用它规避告警。reason 目前不会出现在告警输出里,写它是为了后来的人知道这里为什么可以忽略。
同一个插件里,入口方法与被它调用的内部方法可以都标:安全检查会按调用嵌套组织路径,越靠里越接近真正改变状态的位置。但把标记集中在真正改状态的那一层,告警会更短、更好读。
能保证什么,不能保证什么
检查能做的事很有限,不要把它当成正确性证明。
能保证的:带标记的路径执行了却没有留下录像记录时,控制台会告警,并列出这段执行经过的标记方法;路径按调用嵌套组织,能帮你定位是哪条调用链没有记。
不能保证的:
| 它不检查 | 说明 |
|---|---|
| 逻辑是否正确 | 方法算错了,只要产生了录像记录,检查照样通过 |
| 参数是否正确 | 录像步的参数写错了、写少了,检查不看内容 |
| 顺序是否正确 | 它只关心有没有记录,不比较先后 |
| 是否一一对应 | 一条录像步可以覆盖多个状态修改,一次动作也可能产生多条录像步 |
| 是否漏了入口 | 没有标记的方法不会被登记,自然也不会出现在告警里 |
| 是否会自动记录 | @shouldReplay 只登记路径,记录仍要 record 或内置录制入口 |
最后两条是实践中最容易踩的:没标记的方法,检查器根本不知道它存在;而标了标记的方法,也不会因此自动写录像。
验证
录像一致性最终要靠回放对比:录一段包含目标操作的录像,回放后检查关键数据——位置、属性、道具数量、Flag 值。检查器只管「有没有记」,这里要管「记得对不对」。
如果怀疑某条路径漏记,就给它加上 @shouldReplay,再执行一次对应操作。带标记的方法执行了却没有产生录像记录时,控制台会出现「录像记录缺失」警告;如果没有出现,先确认方法确实被走到、标记确实紧贴在方法上。想看更细的调用路径,就执行警告里附带的 logReplaySafetyDetail(...) 调用;命令形式若有变化,以控制台输出为准。
参数与顺序需要自己核对,装饰器不会替你检查这些。
边界与注意
- 在渲染端加标记。
@shouldReplay只用于数据端;渲染端不参与录像验证,加了也不会进入检查。 - 读档期间录像被禁用。
CoreState.loadState在恢复每个可存档对象之前调用replaySystem.disable(),之后调用revert(),所以读档本身不会被记进录像。 - 执行器不在保护范围内。
bindSaveableExecuter绑定的afterLoad在该对象loadState之后立即运行,此时录像已经恢复可用。在执行器里操作会影响成绩的状态要格外小心,细节见可存档内容。 - 装饰器只认类的方法。写在普通函数、对象属性、箭头函数上都不会生效。
- 不要给只读方法或计算器加标记。它们不产生录像记录,只会让告警列表变长。
- 不要用 @ignoreReplay 压告警。它忽略的是整段检查,真正的漏记会一起被放过。
- 同一个方法只写一个标记。
@shouldReplay与@ignoreReplay同时写含义互相矛盾。 - 异步回调里的记录要处理顺序。录像步会落在错误的时点,检查也看不出来。
- 没有告警不等于没问题。入口没标记、或者检查没有覆盖到这段执行,都不会有告警。
需要自定义录像指令或新的录制入口时,属于 高级 II · 自定义录像行为;录像格式与沙盒原理见 高级 III · 录像深层原理。
小结
- 录像一致性只要求一件事:玩家直接产生、会影响最终成绩的行为,一条都不能少记。
- 该不该记通常靠分析;判断不了时,把所有会改状态、可能影响结果的方法都加上
@shouldReplay。 @shouldReplay只用于数据端,只登记路径;@ignoreReplay把整段检查标记为忽略;两者都不写录像。- 带标记的方法执行了却没有录像记录时,控制台会输出「录像记录缺失」警告。
- 检查不管逻辑、参数、顺序与一一对应,这些要自己核对。
- 读档期间录像被临时禁用,执行器不在保护范围内。
返回高级 I 导览。