漏洞所至,灵肉皆穿——市民撬棒与忍者苦无漏洞背后的故事

  • # 万智牌

大家应该都听说了吧,三月二十一日上线MTGA的依尼翠暗影重铸系列不幸包含一个大漏洞。这个漏洞波及了许多赋予其他物件异能,并且所赋予异能提及自身牌名的卡牌,其中就包括了市民撬棒忍者苦无

这两张牌本该仅牺牲自己,然而这个漏洞导致其操控者异能结算时牺牲所有其操控的永久物。苦无的异每牺牲一个永久物还能多造成3点伤害。

我的天。究竟是怎么回事?我们到底怎么放过了这个漏洞?大家听我娓娓道来。

首先,大家需要对MTGA的编程方式有所了解。我给大家讲解一下最关键的几个方面,再谈谈我们吃了这一堑,长了什么智。

我们的规则引擎代码很大一部分是自动生成的。我们利用自然语言处理器分析每张牌的叙述文字,自动编写代码。单单讲这个处理器就能写一篇甚至一串文章了!我们的自然语言处理器有两个关键特色。其一,每次新系列上线它都会重新分析一次所有卡牌叙述文字,产出代码。我们不会分析完旧卡就再也不动它们的代码了。其二,由于代码是自动生成的,很多片段都是通用的。举个例子大家就明白了。例如

ProposeEffectCostResource【效应费用资源提示】

就是一条代码片段(我们使用的编程语言中管这类片段叫Rule【规则】),用来辨别可用于支付效应费用的资源。每张涉及非法术力的费用的牌都会复用这段代码。这次的漏洞也正是因它而起。

  • “弃一张牌:抓一张牌。”就会使用这段【规则】代码提示你手上的每一张牌。
  • “从你的坟墓场放逐一张红色牌,以作为施放此咒语的额外费用。”会使用这段【规则】提示你坟墓场里的每一张红色牌。
  • “搭载3” 则会用它提示你操控的未横置生物。

 

先把这段代码放一边,我们来看看“赋予其他物件异能,并且所赋予的异能提及自身牌名的卡牌”。塞洛斯:冥途求生的赫利欧德的惩罚是首张上线MTGA的此类卡牌。

这样绕弯的叙述还挺棘手的。大多数异能提及自身时,要么是指“这张牌”,要么是指“将此异能置入堆叠的牌”。而把赫利欧德的惩罚贴给你的后,熊得到了新异能,这个异能会提及自身——却不是熊自身。那是哪个自身?是“将 此被起动的异能 赋予给 起动异能的物件 的牌”。我们希望被赋予给其他永久物的这些异能可以追溯到真正的“自身”。我们认为此类异能基本上都是灵气武具赋予的,于是设计了特殊代码来解决这个问题。

 

我们再回头讲讲效应费用资源。新卡佩纳:喧嚣黑街带来了织契魔冯柯斯帕拉

那么斯帕拉的【效应费用资源提示】代码要怎么写呢?

  • 首先,在你利用斯帕拉的异能施放咒语时,要提示你操控的所有生物上的每类指示物。轻松。然而,你要是同时操控着多个斯帕拉呢?传奇这个名头近来可是越来水了……
  • 我们自然不会按斯帕拉总数提示那么多次,而是只要有一个“利用斯帕拉的异能施放咒语”的动作就行。反正,究竟是用哪一个的异能都一样,我们就干脆不追溯到具体哪个斯帕拉了,要是全显示出来让牌手选用哪一个的异能施放,那才是真的让人摸不着头脑。可是这又带来了新问题……
  • 虽然只有一个斯帕拉的异能起效,可它们每一个都复用了【效应费用资源提示】【规则】。你选择了你操控的生物上的指示物A,每个斯帕拉复用的【规则】都会执行一次,结果就是你从这个生物上移去了等同于斯帕拉数量的指示物A。

还在看,没走吗?太好了!顺带讲一句,我们正在招人呢!

于是为了解决这个问题,我们决定将【效应费用资源提示】【规则】与“卡牌具有的异能”解绑,改为仅与“卡牌异能叙述”绑定——因为所有的斯帕拉的异能叙述都一致,所以这个【规则】只会执行一次。我们在写这条新的【规则】时回顾了我们为赫利欧德的惩罚所设计的特殊代码,在开发尾期又把“卡牌具有的异能”绑回去了,万事大吉。

后来,依尼翠暗影重铸带来了烦人的水沟污迹

水沟污迹和赫利欧德的惩罚一样,赋予其他物件一个提及水沟污迹自身的异能。

然而和赫利欧德的惩罚不一样,它并不是灵气,更不是武具,我们之前设计的特殊代码不好使了,得重新设计。

我们辛勤工作,汗如雨下(也有泪如雨下的时候),最终想出一个办法:在代码生成的过程中,将这类“被赋予给其他物件的异能”中对此异能来源的指代提前,之后的代码生成步骤中【效应费用资源提示】会删去这个指代创建的【规则】中的限制。

这么一来,我们的新漏洞就诞生了。这类异能(由A赋予B且提及A自身)的【效应费用资源提示】【规则】没法追溯到相关异能了。这类【规则】直接提示所有资源,完全不做限制。当然,牺牲这个动作还是隐含“你操控的永久物”这个限制,但如果起动的异能不需要异能操控者选择支付费用的资源,这个【规则】就直接把所有能用于支付费用的资源全支付了。

忍者苦无赋予生物的异能甚至分别提及自身两次。

第一次,“牺牲忍者苦无”中的忍者苦无,就是我们刚讲的提及“赋予了此生物此异能的牌”。

第二次,“忍者苦无对任意一个目标造成3点伤害”,也提及了自身,这个忍者苦无则被理解为等同于以此法被牺牲的永久物。这么一来,忍者苦无的无敌爆炸漏洞就不难理解了。每个以此法牺牲的永久物都被理解成了第二个忍者苦无,于是每个被牺牲的永久物都会造成3点伤害。这个特性其实在别的场景还挺有用的,例如合变生物死亡触发梦魇牧者异能,或者进场触发两次异能一的空境亡灵离场触发异能二。

【译注:这里的意思应该是因为“由此法xx的牌”会追踪所有被xx的牌然后算在一起,所以死亡的合变生物只触发梦魇牧者一次,放逐一整堆合变的牌,派出一个已合变次数为0的死亡生物的1/1复制衍生物;压了一个人两张牌的空境亡灵离场也只触发一次异能二,那个人派出一个X等同于两张牌法术力值之和的X/X衍生物。也有可能我的理解不对,我实在是绕晕了。】

Ian谈给绝望终局伊莫库编程的文章里讲到我们跑了三千多次回归测试,为的就是不出漏洞。那你自然会有疑问了:跑了这么多次,怎么就没把这个漏洞揪出来呢?编写回归测试费神费力,因为本质上是写好一场牌局的剧本,然后用我们的规则引擎跑出来。有些回归测试花一整天还写不完,最最简单的也要起码一刻钟构思、编写、核实。一刻钟听着不算什么,但一个系列几百张牌,堆一起也是个大项目。所以我们不会给每张上MTGA的牌都写一套回归测试,而是重点关注那些需要我们开发特殊处理才能不出问题的牌,其余的新牌在它们初期开发和上线前夕时都时交由我们的品控团队(纯人工)测试。我们也不能要求他们在每个新系列上线MTGA的时候,都再回头把所有的旧系列卡牌全部重新测试一遍吧,是不是?

为一个如此复杂的游戏编写如此宏大的程序,漏洞自然难免。不过爆出这么恶劣的漏洞我们还是挺丧气的,特别是我们的团队为了避免这类漏洞费尽心思,结果还是漏了一个。现在我们修好了这个漏洞,这个修理过程也能拿来作为回归测试的参考。我们还在反思我们的代码分析法,尽量避免将来加入新牌时把老牌弄出漏洞。

最后,我要说,我还是一如既往地为我们的团队在MTGA的开发中作出的贡献而自豪!

原文链接

2023年4月2日 发布于新西兰
全部评论 32条
按时间排序

还没有评论

50 32