Skip to content

05 · 坑与复盘 ​

这一章是完整清单。挑出来的标准:编译通过、跑得动、结果是错的。

一览 ​

#症状根因类型
1判断永远为假,整段效果被跳过无 context 的成员调用,接收者发成 Val.Nothing调用约定
2整张卡抛 KeyNotFound局部变量直读 L["x"];Kismet 局部槽是零初始化语义差异
3条件判断走反宿主规则回调只回 bool,函数返回值被丢掉返回值通道
4遍历型卡拿不到元素Array_Get 走 out 形参,宿主没回写返回值通道
5卡面文本变成类型名EX_TextConst 要按 TextLiteralType 取字段AST 细节
6栈溢出依赖蓝图只当签名表、没转译架构
7栈溢出(第二类)转发壳:A.Foo → B.Foo,B 没有就绕回 A.Foo分派
8编译报 CS0131SetArray/SetSet/SetMap 的属性是赋值目标,被当右值发射发射器
9原语读到错位的参数a[0] 是接收者,实参从 a[1] 起调用约定
10Intel 数值永远为 0语义理解错(三层,见下)数据模型
11触发点"61/61 完整"是假的静态审计把死代码算进去了审计方法
12未实现调用静默变成 0容错设计本身是最大的错误源设计取舍

分类看 ​

A 类:调用约定 —— 最容易反复踩 ​

四条坑(#1 #3 #4 #9)是同一个根源:KARDS 的"返回结果"有两条通道, 参数数组的 0 号位是接收者。

这类错误的特点是:表现为"值不对"而不是"崩了"。

  • 接收者错 → 判断永远为假 → 效果静默跳过
  • out 没回写 → 拿到 Nothing → 当 0 用
  • 返回值丢了 → 条件反转
  • 偏移错位 → 参数全错

防御办法只有一个:写原语时逐个对照 UHT 签名。 void SpawnCardInHandBySide(ESideEnum side, FName card_name, ...) → 那就一定是 a[1] 和 a[2],不能凭感觉写 a[0]。

B 类:源语言语义差异(#2 #5 #8) ​

Kismet 不是 C#,有些默认行为必须显式补齐:

  • 局部槽零初始化 → GetLocal(L, "x") 而不是 L["x"]
  • EX_TextConst 是结构体 → Value.ToString() 得到的是类型名, 要按 TextLiteralType 分支再解包
  • SetArray 的属性是赋值目标 → 不能当右值发射,得走 Assign

判断标准很简单:凡是"看起来像字面量/变量但其实有内部结构"的 AST 节点, 都不能直接 ToString()。

C 类:架构(#6 #7) ​

两个栈溢出都是"看起来能跑"的:

  • 依赖蓝图只当签名表 → 宿主按名字找不到 → 退回同名转发壳 → 绕回自己
  • 单例之间反向指针没建全 → 同上

为什么栈溢出比崩溃更糟

Stack overflow 会直接把进程带走,没有异常可 catch, 自对弈跑到第 300 局才炸,前面的结果全丢。

所以除了修根因,还加了同名调用深度防护(连续 8 次即截断并记为未实现)。 这是纵深防御:根因可能还有没发现的,但至少不会带走进程。

D 类:审计方法(#11) ​

这一条我认为最值得记。

我写完 43 个触发点包装后,静态审计报告"61/61 完整" —— 那是假的, 因为其中 23 个包装函数从来没被调用过,而静态审计扫的是字面量。

核心教训

单一审计维度一定会说谎。 静态覆盖看不见"从没被调用",运行期命中看不见"其实能跑"。 必须多个维度交叉,而且要去追它们互相矛盾的地方 —— 矛盾就是线索。

E 类:设计取舍(#12) ​

宿主对不认识的函数返回 Val.Nothing 继续跑。这是必需的(否则一张卡 能带崩整局),但也是所有静默错误的放大器。

处理方式不是去掉容错,而是让容错可观测:

  • 未实现调用全部登记(函数名 + 次数 + 调用栈)
  • smoke 把未实现当作失败返回非零,而不只是打印
  • apicheck 给出"卡牌逻辑可达的缺口 = 0"这个硬指标

三条通用原则 ​

1. 能拿到结构化数据,就不要用文本 ​

伪代码是人读的,AST 是机器读的。用文本做程序翻译,边界情况永远补不完。

2. 先确认源数据是对的,再动手翻译 ​

反编译器自己的 EX_ComputedJump 边界 bug 修完,效果调用点 +32%。 如果直接拿有 bug 的输出去翻译,翻译器写得再对也白搭。

3. 能失败的断言,比能跑通的程序重要 ​

PASS  Intel 翻牌张数 = cipher          ← 功能对不对
PASS  Intel 不改对手手牌顺序            ← 没破坏别的东西

后一条往往更重要,也更容易被忘。

复盘:当初哪里想错了 ​

想法后来发现
"读卡面文本解析效果就够了"只能覆盖简单卡,且永远有偏差。必须转译
"反编译伪代码可以拿来翻译"有损渲染,边界丢信息。必须走 AST
"触发规则得自己手写"游戏自己就有完整的触发分发表(63 个 Execute*Events)
"自对弈跑通就说明接完了"随机采样抽不到那些卡。要逐卡强制演练
"静态覆盖 61/61 说明接完了"把死代码算进去了。要运行期实测
"Intel 是个布尔标签"是数值字段 cipher,链路横跨三层

最后一条尤其值得记:我在这上面连错三层,每一层都是"看起来合理"的。 数据模型猜错比代码写错更隐蔽 —— 代码错了会崩,模型错了只会一直算错。

下一步 ​

回到 项目页 或 专题总览。