月度归档:2013年12月

炉石传说如何偷看对面的手牌

炉石传说是一款暴雪最新出的卡牌游戏。这里我们简单对它的协议和客户端机制做一下分析。

炉石传说的客户端是采用的Unity3D+C#制作的。画面精良,美术细腻,可以说达到了 Unity3D 引擎作品中相当高的水准。3D的卡牌效果让人惊叹,暴雪出品确实必属精品啊。(Diablo III 除外)

EX1_062_premium由于采用的是Unity3D,可以预见移植到手机和Pad上毫不费力。因为采用的是C#,可以说缺点也是很显然的。C#的反编译非常的简单(虽然C/C++的反编译也难不到哪里去),大大降低了逆向工程(Reverse Engineering)的成本。这里不得不吐槽一下暴雪对微软系列的热爱,Web Server也统统用的IIS+ASP.NET,不出意外Game Server后端也用的是Windows Server以及SQL Server。可能也与暴雪一直专心于制作Windows Game有关。

下面我们不对客户端内容做任何反编译和分析,仅仅从通讯协议层来简单看看炉石传说的客户端在游戏时都做了什么。

我们先在本地用 tcpdump 监听一下 3724和1119端口,通过看包得内容,可以判断炉石传说的游戏 TCP 链接用的是 Protobuf 协议。Protobuf 协议的标志就是开头以 08 或者 0A 开始。

16:03:32.228771 IP 192.168.1.XX.53668 > XXX.XXX.XXX.XX.battlenet: Flags [P.], seq 229:256, ack 11411, win 8192, options [nop,nop,TS val 1131484283 ecr 57022610], length 27
0x0000: 5866 baf3 8757 040c cede b078 0800 4500 Xf...W.....x..E.
0x0010: 004f 6ddf 4000 4006 be15 c0a8 0158 7271 .Om.@.@......Xrq
0x0020: da42 d1a4 0e8c 4ab6 e5de 32f4 32b5 8018 .B....J...2.2...
0x0030: 2000 f655 0000 0101 080a 4371 147b 0366 ...U......Cq.{.f
0x0040: 1892 0200 0000 1300 0000 0811 1004 1800 ................
0x0050: 20ff ffff ffff ffff ffff 0128 00 ...........(.

Protobuf 是 Google 开源的一个公开协议。然而并不是说知道用 Protobuf 就能理解传得是什么内容。Protobuf 需要有详细的 .proto 参数配置文件定义传输的细节,否则对于其它第三方来说就是一团乱码。

通过一些逆向工程,我们大概知道了这个 .proto 参数配置是怎样的。炉石传说可以说是一个瘦客户端。客户端做的计算分析工作很少。这一点很符合暴雪的特点,比如魔兽世界也是这样的一个瘦客户端,大量的3D碰撞检测计算都放在了服务器端。

炉石传说把所有的客户端需要显示的细节,都通过服务器传来数据,其中甚至包含了客户端动画展示时需要的停顿。炉石传说的服务器端维护了一个 id=1~67,并且可以动态增加的字典,其中 1 代表系统,2 和 3 代表两边的玩家,4~35 是 id=2 玩家的所有牌的编号,而 36~67 是 id=3 玩家的所有牌的编号。系统、玩家、牌,这些共用一个属性表,这大大降低了定义的复杂度。

下面是一些通讯的简单例子:

初始化:
player{ ... 玩家属性初始化 ... }
init{ id:4 name:"HERO_03" attr{HEALTH=30} attr{PUT=1} attr{OWNER=1}
init{ id:5 name:"CS2_083b" attr{COST=2} attr{PUT=1} attr{OWNER=1} attr{CASTBY=4}}
init{ id:6 name:"" attr{PUT=2} attr{OWNER=1}}
........
init{ id:35 name:"" attr{PUT=2} attr{OWNER=1}}
init{ id:36 name:"HERO_08" attr{HEALTH=30} attr{PUT=1} attr{OWNER=2}
init{ id:37 name:"CS2_034" attr{COST=2} attr{PUT=1} attr{OWNER=2} attr{CASTBY=36}}
init{ id:38 name:"" attr{PUT=2} attr{OWNER=2}}
........
init{ id:67 name:"" attr{PUT=2} attr{OWNER=2}}

从这个初始化例子我们可以很清晰的看到,服务器给每个客户端初始化了 67 张“牌”,其中 1,2,3 是系统和两边玩家,4,5是玩家A的英雄卡 HERO_03 和英雄技能 CS2_083b,被放到了战场(PUT=1),6-35是玩家A的牌库,被放到了待抽牌堆(PUT=2),相应的,对面36,37是玩家B的英雄卡 HERO_08 和英雄技能 CS2_034,38-67则是玩家B的牌。

接下来的通讯协议分析就不一一说了,总之就是服务器向客户端通过设置这些牌的属性, 让客户端达到显示牌以及动画效果的目的。

那么有人要说笔者标题党了。技术细节看了一大堆,但是如何能看到对面的牌呢?难道服务器会直接在通讯协议里告诉玩家A,玩家B手里的所有牌么?

其实不然。暴雪在技术细节上做的还是很好的。只有在玩家把牌放入战场,对方才能通过通讯协议知道牌的内容。但是其中有一个小问题,就是:

每一次比赛牌的 id 并不会随机化洗牌!

就是说,玩家A和玩家B如果两次用同一副自己组的牌对打,玩家B如果给玩家A出示过某个 id 对应什么牌的话,那么下一次比赛玩家A就会知道玩家B还在手里的牌是什么!

说通俗一点的话,就是对圣斗士来说,每一招只能用一次,第二次用同样的招数就变得很不安全!

下面是一个和同一个好友同一副牌玩第 2 把的例子:

EX1_561_premium敌方 操作 开始
第 1 回合
玩家 铁喙猫头鹰 牌库 -> 手牌
玩家 刺骨 牌库 -> 手牌
玩家 任务达人 牌库 -> 手牌
玩家 刀扇 牌库 -> 手牌
玩家 幸运币 未知 -> 手牌 施法:
敌方 寒冰护体 牌库 -> 手牌
敌方 变形术 牌库 -> 手牌
敌方 魔爆术 牌库 -> 手牌

在第一回合,我们就清楚的知道了对方先手拿得三张牌是什么!

当然,在平时天梯和竞技场里,这个问题可能不会存在,因为每次遇到的选手都是系统随机分配的,都是第一次遇到。如果是打职业线上比赛的话,就难保不会有人通过这个办法作弊咯。

当然,目前游戏还在内测阶段,相信暴雪技术团队应该会在公测前修复这个问题吧。

除此之外,目前的通讯协议上还有下面几个 BUG:

  1. 对面手牌中的”奥秘牌“会在奥秘出示之前就通过通讯协议让另一方知晓。
    就是说虽然客户端没有提示,但是实际上对面一拿到奥秘牌,你的客户端就可以知道对面的哪一张是奥秘牌,虽然不知道具体是什么奥秘。
  2. 服务器错误的把每一方思考时候的选择数目发给了另一方客户端。
    选择数是你每一步有多少种可能。这是服务器端提前算好告诉客户端,然后让客户端提示用户做选择的。比如在1费回合你没有2费牌,只能点”结束回合“,这时候的选择数就是1个。通过判断对方每回合结束的选择数,你可以大致估计对方手牌里的费的多少,以及是不是法术指向牌。
  3. 虽然是第一次遇到某玩家,但是还是可能通过牌的 id 猜到对方某些手牌的内容。
    注意,因为没有通过随机化洗牌(shuffle),所以牌的 id 实际上是玩家从牌库中选择的顺序。有些玩家喜欢从1费开始选牌,有些喜欢从高费开始,但是一般两张同样的牌都会一起选进去,所以他们拥有相邻的id。比如你看见对方出了一张id=16的炎爆术,手里还有一张id=15的未知牌,那么你就知道下回合多半又是一张炎爆打脸了。

好了,简单写了这些,其实并没有太严重到逆天的 BUG,炉石传说还是一款好游戏。希望暴雪团队继续努力,早点出移动客户端,让笔者可以早点躺着玩。