最近看Base64的时候看到了知乎上一个关于炉石卡组代码的帖子,它本质上就是用一个Base64编码的字符串来存储卡组信息。你有时会见到以=或者==结尾的卡组代码,这正是Base64编码的典型特征。
DBF ID
为了了解编码的机制,我们需要先介绍DBF ID。这是每张炉石卡牌(包括可收集卡牌、衍生卡牌、冒险模式专属卡牌、英雄皮肤等)的唯一标识符。而炉石卡组代码正是使用DBF ID来表示每张卡牌。这也是能够卡出下图这种 BUG 的原因,皮肤被标记为了一张紫卡。
DBF ID可在游戏文件中找到。更加方便的方法是通过 HearthstoneJSON ,这上面提供了相关API。也可以在这个网站上直接下载最新的cards.collectible.json
,这里面包含了所有可收集卡牌的信息,例如各种语言的卡牌名称、描述等。
格式
如前所述,卡组代码是 Base64 编码的字符串。我们先来解码它,许多编程语言都可以做到这一点,以js 为例:
function parse_deckstring(deckstring) {
let binary = Buffer.from(deckstring, "base64");
let hex = binary.toString("hex");
let arr = hex.match(/.{1,2}/g);
return arr.map(x => parseInt(x, 16));
}
这个函数会先将Base64字符串解码成2进制,然后转换成16进制,再按每两个元素切割,最后从16进制字符串转化为整型并返回。也就是说,它们可以是 0x00-0xff,即 0-255。
现在我们得到的是一个由整数组成的数组。更具体的来说,就是 varint。还需要进一步的解码,才能读取出有用的信息:
function read_varint(data) {
let shift = 0;
let result = 0;
let c;
do {
c = data.shift();
result |= (c & 0x7f) << shift;
shift += 7;
}
while (c & 0x80);
return result;
}
这里的data.shift()
会将数组的第一个元素移出数组,并返回它的值。read_varint()
函数完成了解码过程,不断地执行read_varint()
,直到取完arr中的元素,就能把内容全部解码出来。
根据作用,可以把解码后的卡组代码分为两个部分:元数据块和卡牌块。
元数据块
在arr数组中,前五个元素分别为:
- 保留字节 0x00
- 版本号(固定为 1)
- 模式(1 为狂野,2 为标准)
- 使用英雄卡牌的数量(固定为 1)
- 使用的英雄卡牌的类型(长度不确定,一般是 1-3 位)
可见卡组代码以字节 0x00 开头。然后是编码版本号,目前始终为 1。虽然这五个元素并没有官方名称,不过从作用上可以看作元数据。
卡牌块
在元数据块之后,继续读取arr的元素,那么接下来就轮到卡牌块了。它按以下顺序分为三对长度 + 数组的组合:
- 卡组中存在一张的卡
- 卡组中存在两张的卡
- 卡组中存在 n 张的卡
每张卡都用 varint 型的 DBF ID 表示。
所谓「卡组中存在 n 张的卡」,指的是卡组中的所有其他卡牌。此数组是一个 varint 对组成的列表,每一对的第一个元素表示 DBF ID,第二个元素是该卡牌在卡组中出现的次数。它应该只包含在牌组中至少出现三次的牌,因而意味着它(在这种情况下)对于构筑卡组应当是空的(毕竟一张卡最多只能带两张)。
尽管最终排序无关紧要,但卡牌仍会在各自所在的数组中,按 DBF ID 的升序进行排序,以便始终为相同的卡组生成相同的卡组代码。我们称所有按照这种方式排列卡牌列表(包括英雄)的卡组代码为规范的卡组代码。下面的参考实现应该已经可以生成规范的卡组代码:
function parse_deck(data) {
let reserve = read_varint(data);
if (reserve !== 0) {
return "Invalid deckstring";
}
let version = read_varint(data);
if (version !== 1) {
return "Unsupported deckstring version " + version;
}
let format = read_varint(data);
let heroes = [];
let num_heroes = read_varint(data);
for (let i = 0; i < num_heroes; i++) {
heroes.push(read_varint(data));
}
let cards = [];
let num_cards_x1 = read_varint(data);
for (let i = 0; i < num_cards_x1; i++) {
card_id = read_varint(data);
cards.push([card_id, 1]);
}
let num_cards_x2 = read_varint(data);
for (let i = 0; i < num_cards_x2; i++) {
card_id = read_varint(data);
cards.push([card_id, 2]);
}
let num_cards_xn = read_varint(data);
for (let i = 0; i < num_cards_xn; i++) {
let card_id = read_varint(data);
let count = read_varint(data);
cards.push([card_id, count]);
}
return {
cards,
heroes,
format
};
}
这时,再通过建立 DBF ID与卡牌对应关系的数据库,就能够实现在游戏外导入、导出和编辑卡组的功能了。不论是官方的卡牌工具,还是第三方平台(如旅法师营地、盒子)的套牌编辑功能,原理都是相似的。